Aller au contenu principal
Certyneo
Documentation développeur

Webhooks — recevez les événements de signature en temps réel

Configurez une URL HTTPS dans votre dashboard Certyneo et recevez un POST signé HMAC-SHA256 dès qu'un événement se produit sur vos enveloppes : signature, refus, expiration. 11 événements supportés, 5 tentatives de livraison en backoff exponentiel, vérification cryptographique en 8 lignes de code.

< 5s

Délai médian de livraison après l'événement

5x

Tentatives de livraison au total, étalées sur environ 1 h 20

HMAC-SHA256

Algorithme de signature de chaque requête

Catalogue des événements

Les 11 événements ci-dessous couvrent l'intégralité du cycle de vie d'une enveloppe Certyneo. Activez ceux qui vous intéressent dans Paramètres → Webhooks, ignorez les autres — l'abonnement est granulaire par événement.

ÉvénementDéclenchement
envelope.createdUne enveloppe est créée (par UI, API ou template) — utile pour synchroniser un enregistrement côté CRM dès la création.
envelope.sentL'enveloppe est envoyée aux signataires (premier email envoyé). Marque le début du cycle de signature actif.
envelope.completedTous les signataires ont signé et le PDF scellé eIDAS est stocké. Le payload porte signedDocumentUrl, un lien pré-signé valable 7 jours ; à défaut, GET /v1/envelopes/id/signed-document, et la piste d'audit via GET /v1/envelopes/id/audit-trail.
envelope.declinedUn signataire a refusé l'enveloppe. L'adresse du refusant est dans `data.declinedBy` et la raison, si elle a été saisie, dans `data.reason`.
envelope.voidedL'enveloppe a été annulée par l'émetteur avant signature complète. Distinct de `expired` (humain vs timeout).
envelope.expiredLa date d'expiration de l'enveloppe est passée sans signature complète. Interrogez GET /v1/envelopes/id pour connaître les signataires manquants.
envelope.returned_to_senderUn signataire a renvoyé l'enveloppe à l'émetteur pour correction, sans la refuser. La raison est dans `data.reason` et l'auteur du renvoi dans `data.returnedBy`.
envelope.resubmittedL'émetteur a corrigé puis renvoyé une enveloppe précédemment retournée. Marque la reprise du cycle de signature.
recipient.signedUn signataire individuel a signé (mais pas forcément tous). Utile pour suivre l'avancement et enchaîner sur l'étape suivante d'un workflow séquentiel. Attention : le PDF scellé n'existe pas encore à ce stade, même pour le dernier signataire — un téléchargement lancé ici renvoie un HTTP 409. Passez par envelope.completed pour le document.
recipient.viewedUn signataire a ouvert le lien de signature sans encore signer. Utile pour les relances commerciales ciblées.
recipient.approvedUn approbateur a validé l'enveloppe sans y apposer de signature (workflow de validation interne). L'adresse est dans `data.approvedBy`.

recipient.signed ne signifie pas « document disponible »

recipient.signed est émis pour CHAQUE signataire, au moment où il termine sa signature — y compris le dernier, avant que le PDF scellé ne soit assemblé et stocké. Un téléchargement déclenché depuis ce handler reçoit donc toujours un HTTP 409 « Signed document not available until the envelope is COMPLETED ». Ce n'est pas une erreur : c'est un « pas encore prêt ». Abonnez-vous à envelope.completed pour récupérer le document, et gardez recipient.signed pour suivre l'avancement (qui a signé, et quand).

Format du payload

Toutes les livraisons partagent le même schéma JSON top-level : `event`, `data` et `timestamp`. Le nom de l'événement est aussi répété dans l'en-tête `X-Certyneo-Event`, ce qui permet de router avant même de parser le corps. Le contenu de `data` varie selon l'événement, mais reste toujours un objet plat de valeurs simples — jamais de tableau ni d'objet imbriqué. Voici une livraison `envelope.completed` complète.

POST /webhooks/certyneo HTTP/1.1
Host: your.app
Content-Type: application/json
X-Certyneo-Event: envelope.completed
X-Certyneo-Signature: 4f3d1c8b2a9e7f60d5c4b3a2918e7f6d5c4b3a2918e7f6d5c4b3a2918e7f6d5c
{
  "event": "envelope.completed",
  "data": {
    "envelopeId": "cm7x2k9p40001qz8h3f7bn2ld",
    "subject": "Contrat de prestation Acme Corp",
    "status": "COMPLETED",
    "completedAt": "2026-05-27T08:42:13.000Z",
    "recipientCount": 2,
    "recipients": [
      { "email": "alice@acme.com", "name": "Alice Martin", "role": "SIGNER", "status": "SIGNED" },
      { "email": "bob@acme.com", "name": "Bob Durand", "role": "SIGNER", "status": "SIGNED" }
    ],
    "signedDocumentUrl": "https://storage.certyneo.com/signed/cm7x2k9p4.../contrat.pdf?X-Amz-Expires=604800&X-Amz-Signature=..."
  },
  "timestamp": "2026-05-27T08:42:13.521Z"
}

signedDocumentUrl est un lien pré-signé valable 7 jours à compter de l'événement : il évite un second appel authentifié pour récupérer le PDF. Il est absent — et non pas null — si la pré-signature a échoué, ou sur une enveloppe QES complétée sans document stocké ; repassez alors par GET /v1/envelopes/id/signed-document, qui reste la source de vérité. Attention aussi au rejeu manuel depuis la dead-letter queue plus de 7 jours après l'événement : le lien du payload est expiré, l'endpoint API non.

Le payload est encodé UTF-8, sans BOM. La signature HMAC est calculée sur le body brut tel qu'envoyé — n'altérez pas les espaces, le re-parsing JSON modifie souvent l'ordre des clés et casse la vérification.

Vérifier la signature HMAC

Chaque requête est signée avec votre secret webhook (affiché une seule fois à la création de l'abonnement). La signature est transmise dans l'en-tête `X-Certyneo-Signature` : c'est le HMAC-SHA256 du corps brut de la requête, encodé en hexadécimal, sans préfixe ni horodatage. Vérifiez TOUJOURS la signature avant de traiter le payload — sans cette étape, n'importe qui peut forger un événement et appeler votre endpoint.

Node.js / TypeScript

import crypto from "node:crypto";

export function verifyCertyneoSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string,
): boolean {
  // X-Certyneo-Signature = HMAC-SHA256(secret, rawBody), hex-encoded.
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  const received = Buffer.from(signatureHeader, "hex");
  const computed = Buffer.from(expected, "hex");

  // timingSafeEqual throws when the two buffers differ in length —
  // a malformed header must return false, not crash the handler.
  if (received.length !== computed.length) return false;

  // timingSafeEqual to mitigate timing attacks.
  return crypto.timingSafeEqual(computed, received);
}

Python

import hashlib
import hmac


def verify_certyneo_signature(
    raw_body: bytes, signature_header: str, secret: str
) -> bool:
    """Verify a Certyneo webhook signature.

    `X-Certyneo-Signature` holds the hex-encoded HMAC-SHA256 of the
    raw request body, computed with your webhook secret.
    """
    expected = hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, signature_header)

Erreur fréquente

N'utilisez PAS `===` ou `==` pour comparer la signature attendue à celle reçue. Utilisez une fonction timing-safe (`crypto.timingSafeEqual` en Node, `hmac.compare_digest` en Python). Sans ça, l'écart de temps de comparaison entre deux signatures révèle progressivement le secret à un attaquant patient (timing attack).

Politique de retry

Si votre endpoint met trop de temps à répondre, refuse la connexion ou renvoie un 5xx (ou un 429), nous retentons selon un backoff exponentiel : 5 tentatives au total, sur environ 1 h 20. Passé la cinquième, l'événement part en dead-letter queue — consultable et rejouable depuis votre dashboard, mais plus retenté automatiquement. Un refus explicite (401, 403, 404, 410, 422…) n'est en revanche jamais retenté : la réponse ne changerait pas, l'événement part directement en dead-letter queue.

TentativeDélai avant la tentativeTemps écoulé depuis l'événement
#100
#2+ 1 min1 min
#3+ 5 min6 min
#4+ 15 min21 min
#5+ 1 h1 h 21

Les délais indiqués sont des minimums : le balayage de retry est cadencé par un cron, une tentative peut donc partir légèrement après l'heure théorique. Les événements abandonnés sont listés dans Webhooks → Échecs, avec un bouton de rejeu manuel et sans limite de durée de conservation. Un endpoint qui enchaîne cinq échecs définitifs — ou qui répond 404 / 410 — est désactivé automatiquement, et vous en êtes prévenu par email : réactivez-le une fois corrigé, le compteur repart de zéro (toute livraison réussie le remet également à zéro).

Tester sans envoyer de vraie enveloppe

Créez d'abord un abonnement — la réponse contient le `secret` nécessaire à la vérification HMAC, affiché une seule fois. Depuis Paramètres → Webhooks, le bouton « Tester » envoie ensuite un POST signé exactement comme une vraie livraison et vous affiche la réponse brute de votre serveur : c'est le moyen le plus rapide de valider votre handler, en local via ngrok ou en intégration continue.

# Create a subscription — "secret" is returned once, store it now.
curl -X POST https://api.certyneo.com/v1/webhooks \
  -H "Authorization: Bearer $CERTYNEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your.app/webhooks/certyneo",
    "events": ["envelope.completed", "recipient.signed"]
  }'

L'URL doit être publiquement joignable en HTTPS : une adresse privée ou localhost est rejetée par la protection SSRF au moment de la création de l'abonnement.

6 pratiques à respecter

  • Vérifier la signature HMAC AVANT toute lecture du body — utiliser une comparaison timing-safe.
  • Traiter chaque `data.envelopeId` × `event` une seule fois en stockant les couples déjà vus en base — un retry peut relivrer un événement que vous avez déjà traité si votre 2xx s'est perdu.
  • Répondre HTTP 2xx sous 10 secondes maximum, puis traiter en asynchrone (queue). Au-delà, la livraison est coupée et comptée comme un échec.
  • Logger le body brut + la signature complète en debug — la vérification HMAC échoue souvent sur un BOM ou un whitespace invisible.
  • Surveiller la page Webhooks → Échecs : vous êtes prévenu par email si votre endpoint est désactivé, mais pas événement par événement — un événement qui épuise ses tentatives, c'est à vous d'aller le rejouer.
  • Filtrer les événements à la souscription plutôt que dans votre handler, et rester sous la limite de 5 abonnements par compte.

Vous n'êtes pas obligé d'héberger un point de terminaison pour recevoir ces évènements : le connecteur pose l'abonnement pour vous et démarre le flux directement sur un évènement d'enveloppe. Voir Certyneo pour Power Automate et Microsoft 365.

Pour aller plus loin

Prêt à connecter vos systèmes ?

Les webhooks et l'API REST sont inclus à partir du plan Standard. Créez votre compte, générez une clé `sk_test_` et branchez votre endpoint en sandbox avant de passer en production.