Webhooks
Recevoir les événements submission en push HTTP signé HMAC plutôt qu'en polling.
Les webhooks Inkr envoient un POST à votre endpoint à chaque événement (création, signature, complétion, etc.) avec une signature HMAC-SHA256 pour vérifier l'authenticité.
Events disponibles
8 events sont souscriptibles :
| Event | Déclenchement |
|---|---|
submission.created | Submission créée via API. |
submission.sent | Email envoyé au premier submitter. |
submission.viewed | Submitter a ouvert le signing URL. |
submission.partially_signed | Au moins un submitter (mais pas tous) a signé. |
submission.completed | Tous les submitters ont signé, PDF final prêt. |
submission.declined | Submitter a refusé. |
submission.expired | expires_at dépassé. |
submission.cancelled | Annulée via DELETE. |
Granularité submitter. Les events submitter.opened, submitter.signed et submitter.pre_signed existent dans l'audit trail mais ne sont pas livrables par webhook. Tenter de les souscrire renvoie 422 invalid_events. Pour les consulter, interrogez GET /v1/events.
Créer un endpoint
Depuis le dashboard developers.getinkr.eu/dev/webhooks ou via API :
curl -X POST https://api.getinkr.eu/v1/webhook_endpoints \
-H "Authorization: Bearer $INKR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"url": "https://app.example.com/webhooks/inkr",
"events": ["submission.completed", "submission.declined"]
}'La réponse retourne un secret (format whsec_<32 base62>) à utiliser pour la vérification HMAC. Affiché une seule fois, jamais re-exposé par GET.
Le body est strict : seules les clés url et events sont acceptées, toute clé supplémentaire renvoie 422. Aucune Idempotency-Key n'est requise sur cet endpoint. Limite : 25 endpoints actifs par compte.
Autres endpoints webhook : GET /v1/webhook_endpoints (liste paginée), GET /v1/webhook_endpoints/{id} (détail et état de santé), DELETE /v1/webhook_endpoints/{id} (désactivation) et POST /v1/webhook_endpoints/{id}/test (envoi d'un event webhook.test, réponse brute de votre serveur incluse).
Payload reçu
{
"id": "evt_01J5HZ...",
"type": "submission.completed",
"created_at": "2026-05-15T14:32:08.421Z",
"data": {
"object": {
"id": "sub_01J5HZ...",
"status": "completed",
"template_id": "tpl_01J5HZ...",
"metadata": { "internal_contract_id": "k_42" },
"submitters": [
{
"id": "sbm_01J5HZ...",
"email": "alice@example.com",
"external_id": "partner_42",
"status": "signed",
"signed_at": "2026-05-15T14:31:45.000Z"
}
],
"audit_log_url": "https://api.getinkr.eu/v1/submissions/sub_01J5HZ.../audit_log",
"completed_at": "2026-05-15T14:32:08.421Z"
}
}
}Vérification de signature HMAC
Inkr signe chaque payload avec votre secret. Le header Inkr-Signature contient le timestamp UNIX + signature HMAC-SHA256, style Stripe :
Inkr-Signature: t=1715782328,v1=4eac7f5e9d2b...Vérifiez côté votre serveur :
import crypto from 'node:crypto'
function verifyInkrSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
header.split(',').map((kv) => {
const i = kv.indexOf('=')
return [kv.slice(0, i).trim(), kv.slice(i + 1).trim()]
}),
)
const timestamp = Number.parseInt(parts.t, 10)
const signature = parts.v1
if (!Number.isFinite(timestamp) || !/^[0-9a-f]{64}$/.test(signature)) return false
// Anti-rejeu : refuser les timestamps trop anciens.
const now = Math.floor(Date.now() / 1000)
if (Math.abs(now - timestamp) > toleranceSeconds) return false
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`, 'utf8')
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(signature, 'hex'),
)
}import hashlib
import hmac
import re
import time
def verify_inkr_signature(raw_body: bytes, header: str, secret: str,
tolerance_seconds: int = 300) -> bool:
parts = dict(kv.split('=', 1) for kv in header.split(','))
try:
timestamp = int(parts['t'].strip())
except (KeyError, ValueError):
return False
signature = parts.get('v1', '').strip()
if not re.fullmatch(r'[0-9a-f]{64}', signature):
return False
# Anti-rejeu : refuser les timestamps trop anciens.
if abs(time.time() - timestamp) > tolerance_seconds:
return False
payload = f'{timestamp}.'.encode() + raw_body
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)require 'openssl'
def verify_inkr_signature(raw_body, header, secret, tolerance_seconds = 300)
parts = header.split(',').to_h { |kv| kv.split('=', 2).map(&:strip) }
timestamp = Integer(parts['t'], exception: false)
signature = parts['v1'].to_s
return false unless timestamp && signature.match?(/\A[0-9a-f]{64}\z/)
# Anti-rejeu : refuser les timestamps trop anciens.
return false if (Time.now.to_i - timestamp).abs > tolerance_seconds
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, "#{timestamp}.#{raw_body}")
OpenSSL.secure_compare(expected, signature)
endImportant : utilisez le raw body (pas le JSON parsé) pour calculer le HMAC. La moindre normalisation casse la signature.
Retry et failing
Si votre endpoint répond 2xx, le webhook est delivered. Sinon, Inkr retente selon ce schedule :
- 0s (immédiat)
- 30s
- 5 min
- 30 min
- 2h
- 6h
- 24h
- 72h
Soit 8 tentatives sur 72h total. Après 8 échecs consécutifs : failing=true + email à l'owner de l'API key + désactivation automatique après 7 jours sans succès.
Vous pouvez re-tester un endpoint depuis le dashboard et le réactiver après fix.
Bonnes pratiques
- Idempotence côté votre serveur : un même
event_idpeut être envoyé plusieurs fois en cas de retry. Gardez une tableprocessed_event_idscôté vous. - Ack rapide : répondez
200dans les 5 secondes. Traitez le payload en async (queue/worker). - Plusieurs endpoints : vous pouvez créer N webhook_endpoints différents avec subsets d'events si vous voulez dispatcher.
metadatapour matcher : passez votreinternal_contract_iddanssubmission.metadataà la création, retrouvez-le dans le webhook payload pour matcher avec votre DB.