Webhooks
Recevoir les événements submission en push HTTP signé HMAC plutôt qu'en polling.
Les webhooks Inkr envoient un POST à ton endpoint à chaque événement (création, signature, complétion, etc.) avec une signature HMAC-SHA256 pour vérifier l'authenticité.
Events disponibles
10 events granulaires :
| 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. |
submitter.opened | Submitter individuel a ouvert. |
submitter.signed | Submitter individuel a signé. |
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 (whsec_xxx) à utiliser pour la vérification HMAC. Affiché une seule fois.
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 ton secret. Le header Inkr-Signature contient le timestamp UNIX + signature HMAC-SHA256, style Stripe :
Inkr-Signature: t=1715782328,v1=4eac7f5e9d2b...Vérifie côté ton serveur :
import crypto from 'node:crypto'
function verifyInkrSignature(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(',').map((kv) => kv.split('=')),
)
const timestamp = parts.t
const signature = parts.v1
const payload = `${timestamp}.${rawBody}`
const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex')
// Constant-time comparison
const sigBuf = Buffer.from(signature, 'hex')
const expBuf = Buffer.from(expected, 'hex')
if (sigBuf.length !== expBuf.length) return false
return crypto.timingSafeEqual(sigBuf, expBuf)
}
// Tolérance horloge 5 minutes pour rejeter les replays
function isFreshTimestamp(header, maxAgeSeconds = 300) {
const t = parseInt(header.split(',')[0].split('=')[1], 10)
return Math.abs(Date.now() / 1000 - t) < maxAgeSeconds
}import hashlib
import hmac
import time
def verify_inkr_signature(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(kv.split('=', 1) for kv in header.split(','))
timestamp = parts['t']
signature = parts['v1']
payload = f'{timestamp}.{raw_body.decode()}'.encode()
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)
def is_fresh_timestamp(header: str, max_age_seconds: int = 300) -> bool:
t = int(header.split(',')[0].split('=')[1])
return abs(time.time() - t) < max_age_secondsrequire 'openssl'
def verify_inkr_signature(raw_body, header, secret)
parts = header.split(',').to_h { |kv| kv.split('=', 2) }
timestamp = parts['t']
signature = parts['v1']
payload = "#{timestamp}.#{raw_body}"
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, payload)
Rack::Utils.secure_compare(signature, expected)
end
def fresh_timestamp?(header, max_age_seconds = 300)
t = header.split(',').first.split('=', 2).last.to_i
(Time.now.to_i - t).abs < max_age_seconds
endImportant : utilise le raw body (pas le JSON parsé) pour calculer le HMAC. La moindre normalisation casse la signature.
Retry et failing
Si ton 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.
Tu peux re-tester un endpoint depuis le dashboard et le réactiver après fix.
Bonnes pratiques
- Idempotence côté ton serveur : un même
event_idpeut être envoyé plusieurs fois en cas de retry. Garde une tableprocessed_event_idscôté toi. - Ack rapide : réponds
200dans les 5 secondes. Traite le payload en async (queue/worker). - Plusieurs endpoints : tu peux créer N webhook_endpoints différents avec subsets d'events si tu veux dispatcher.
metadatapour matcher : passe toninternal_contract_iddanssubmission.metadataà la création, retrouve-le dans le webhook payload pour matcher avec ta DB.