الانتقال إلى المحتوى الرئيسي
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. 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é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é. Le PDF scellé eIDAS et l'audit trail sont disponibles via `proof_pdf_url` dans le payload.
envelope.declinedUn signataire a refusé l'enveloppe. La raison de refus (si fournie par le signataire) est dans `data.decline_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. La liste des signataires manquants est dans `data.missing_signers`.
recipient.signedUn signataire individuel a signé (mais pas forcément tous). Utile pour les workflows séquentiels : déclencher le passage au signataire suivant.
recipient.viewedUn 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é.

TentativeDélai cumuléTemps écoulé
#100
#2+ 1 min1 min
#3+ 5 min6 min
#4+ 30 min36 min
#5+ 2 h2h 36
#6+ 6 h8h 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).