Inkr API

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 :

EventDéclenchement
submission.createdSubmission créée via API.
submission.sentEmail envoyé au premier submitter.
submission.viewedSubmitter a ouvert le signing URL.
submission.partially_signedAu moins un submitter (mais pas tous) a signé.
submission.completedTous les submitters ont signé, PDF final prêt.
submission.declinedSubmitter a refusé.
submission.expiredexpires_at dépassé.
submission.cancelledAnnulée via DELETE.
submitter.openedSubmitter individuel a ouvert.
submitter.signedSubmitter 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_seconds
require '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
end

Important : 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_id peut être envoyé plusieurs fois en cas de retry. Garde une table processed_event_ids côté toi.
  • Ack rapide : réponds 200 dans 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.
  • metadata pour matcher : passe ton internal_contract_id dans submission.metadata à la création, retrouve-le dans le webhook payload pour matcher avec ta DB.