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énement | Déclenchement |
|---|---|
envelope.created | Une enveloppe est créée (par UI, API ou template) — utile pour synchroniser un enregistrement côté CRM dès la création. |
envelope.sent | L'enveloppe est envoyée aux signataires (premier email envoyé). Marque le début du cycle de signature actif. |
envelope.completed | Tous 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.declined | Un 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.voided | L'enveloppe a été annulée par l'émetteur avant signature complète. Distinct de `expired` (humain vs timeout). |
envelope.expired | La 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_sender | Un 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.resubmitted | L'émetteur a corrigé puis renvoyé une enveloppe précédemment retournée. Marque la reprise du cycle de signature. |
recipient.signed | Un 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.viewed | Un signataire a ouvert le lien de signature sans encore signer. Utile pour les relances commerciales ciblées. |
recipient.approved | Un 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.
| Tentative | Délai avant la tentative | Temps écoulé depuis l'événement |
|---|---|---|
| #1 | 0 | 0 |
| #2 | + 1 min | 1 min |
| #3 | + 5 min | 6 min |
| #4 | + 15 min | 21 min |
| #5 | + 1 h | 1 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.