Inkr API

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 :

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.

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)
end

Important : 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_id peut être envoyé plusieurs fois en cas de retry. Gardez une table processed_event_ids côté vous.
  • Ack rapide : répondez 200 dans 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.
  • metadata pour matcher : passez votre internal_contract_id dans submission.metadata à la création, retrouvez-le dans le webhook payload pour matcher avec votre DB.