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. 8 événements supportés, retry exponentiel sur 6 tentatives, vérification cryptographique en 8 lignes de code.
< 5s
Délai médian de livraison après l'événement
5x
Tentatives en cas d'échec (jusqu'à ~9h plus tard)
HMAC-SHA256
Algorithme de signature de chaque requête
Catalogue des événements
Les 8 é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é. Le PDF scellé eIDAS et l'audit trail sont disponibles via `proof_pdf_url` dans le payload. |
envelope.declined | Un signataire a refusé l'enveloppe. La raison de refus (si fournie par le signataire) est dans `data.decline_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. La liste des signataires manquants est dans `data.missing_signers`. |
recipient.signed | Un signataire individuel a signé (mais pas forcément tous). Utile pour les workflows séquentiels : déclencher le passage au signataire suivant. |
recipient.viewed | Un signataire a ouvert le lien de signature sans encore signer. Utile pour les relances commerciales ciblées. |
Format du payload
Tous les événements partagent le même schéma JSON top-level : `event`, `envelope_id`, `occurred_at`, `data`. Le contenu de `data` varie selon l'événement (champs documentés par événement dans la référence API). Voici un exemple `envelope.completed` complet.
{
"event": "envelope.completed",
"envelope_id": "env_01HG3K8X9Y2N4P5Q6R7S8T9V0W",
"occurred_at": "2026-05-27T08:42:13.521Z",
"data": {
"title": "Contrat de prestation Acme Corp",
"status": "completed",
"created_at": "2026-05-24T14:12:00.000Z",
"completed_at": "2026-05-27T08:42:13.000Z",
"signers": [
{
"email": "client@acme.example",
"name": "Alex Client",
"signed_at": "2026-05-27T08:42:13.000Z",
"method": "eidas_aes",
"ip": "203.0.113.42"
}
],
"proof_pdf_url": "https://certyneo.com/api/envelopes/env_01HG.../proof.pdf"
}
}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 (visible une seule fois lors de la création dans le dashboard, puis chiffré au repos). La signature est transmise dans l'en-tête `Certyneo-Signature` au format `t=<timestamp>,v1=<hex_hmac>`. 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 {
// Header format: t=<timestamp>,v1=<hex_hmac>
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("=")),
);
const ts = parts.t;
const sig = parts.v1;
if (!ts || !sig) return false;
// Reject events older than 5 minutes (replay protection).
const age = Math.abs(Date.now() / 1000 - Number(ts));
if (age > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
// timingSafeEqual to mitigate timing attacks.
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(sig, "hex"),
);
}Python
import hashlib
import hmac
import time
def verify_certyneo_signature(
raw_body: bytes, signature_header: str, secret: str
) -> bool:
"""Verify a Certyneo webhook signature.
Header format: `t=<timestamp>,v1=<hex_hmac>`
Rejects events older than 5 minutes (replay protection).
"""
parts = dict(p.split("=") for p in signature_header.split(","))
ts = parts.get("t")
sig = parts.get("v1")
if not ts or not sig:
return False
if abs(time.time() - int(ts)) > 300:
return False
expected = hmac.new(
secret.encode("utf-8"),
f"{ts}.{raw_body.decode('utf-8')}".encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, sig)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 répond autre chose qu'un HTTP 2xx (timeout, 5xx, connection refused), nous retentons selon un backoff exponentiel. Au total 6 tentatives sur ~9 heures. Au bout des 6, l'événement est marqué `failed` dans votre dashboard webhook et un email d'alerte est envoyé.
| Tentative | Délai cumulé | Temps écoulé |
|---|---|---|
| #1 | 0 | 0 |
| #2 | + 1 min | 1 min |
| #3 | + 5 min | 6 min |
| #4 | + 30 min | 36 min |
| #5 | + 2 h | 2h 36 |
| #6 | + 6 h | 8h 36 |
Si vous voulez relayer manuellement un événement raté, le dashboard webhook propose un bouton « Re-deliver » sur chaque livraison échouée — disponible pendant 7 jours après l'échec définitif.
Tester sans envoyer de vraie enveloppe
L'endpoint `/v1/webhooks/test` accepte une URL et un type d'événement, et POSTe un payload fictif signé exactement comme un vrai. Idéal pour valider votre handler en intégration continue ou pendant le développement local (avec ngrok ou équivalent).
# Trigger a test webhook to your endpoint
curl -X POST https://api.certyneo.com/v1/webhooks/test \
-H "Authorization: Bearer $CERTYNEO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"event": "envelope.completed", "url": "https://your.app/webhooks/certyneo"}'6 pratiques à respecter
- Vérifier la signature HMAC AVANT toute lecture du body — utiliser une comparaison timing-safe.
- Traiter chaque `envelope_id` × `event` une seule fois en stockant les IDs déjà vus en base (les retries peuvent re-livrer le même événement).
- Répondre HTTP 200 sous 5 secondes maximum, puis traiter en asynchrone (queue). Sinon vous serez en timeout et compté comme retry.
- Logger le body brut + la signature complète en debug — la vérification HMAC échoue souvent sur un BOM ou un whitespace invisible.
- Brancher une dead-letter queue sur les événements `failed` pour relayer manuellement en cas d'incident côté votre service.
- Tolérer un décalage de 5 minutes entre `t=` et l'heure de réception — au-delà, rejeter pour bloquer les replays.
Pour aller plus loin
Prêt à connecter vos systèmes ?
Créez un compte gratuit en 2 minutes — la configuration webhook est disponible dès le plan Starter (gratuit, 5 enveloppes/mois).