Zum Hauptinhalt springen
Certyneo
Entwicklerdokumentation

Webhooks — Empfangen Sie Signaturedeignisse in Echtzeit

Konfigurieren Sie eine HTTPS-URL in Ihrem Certyneo-Dashboard und erhalten Sie einen mit HMAC-SHA256 signierten POST, sobald ein Ereignis in Ihren Umschlägen auftritt: Signatur, Ablehnung, Ablauf. 8 unterstützte Ereignisse, exponentielles Retry über 6 Versuche, kryptographische Verifizierung in 8 Codezeilen.

< 5s

Durchschnittliche Lieferfrist nach dem Ereignis

5x

Wiederholungsversuche bei Fehler (bis zu ~9h später)

HMAC-SHA256

Signaturalgorithmus für jede Anfrage

Ereigniskatalog

Die 8 unten stehenden Ereignisse decken den gesamten Lebenszyklus eines Certyneo-Umschlags ab. Aktivieren Sie diejenigen, die Sie interessieren, in Einstellungen → Webhooks, ignorieren Sie die anderen — das Abonnement ist granular pro Ereignis.

EreignisAuslöser
envelope.createdEine Umschlag wird erstellt (über UI, API oder Vorlage) — nützlich, um einen CRM-Datensatz bei der Erstellung zu synchronisieren.
envelope.sentDer Umschlag wird an die Unterzeichner gesendet (erste E-Mail gesendet). Markiert den Beginn des aktiven Unterzeichnungszyklus.
envelope.completedAlle Unterzeichner haben unterzeichnet. Das versiegelte eIDAS-PDF und der Audit Trail sind über `proof_pdf_url` in der Payload verfügbar.
envelope.declinedEin Unterzeichner hat den Umschlag abgelehnt. Der Ablehnungsgrund (falls vom Unterzeichner angegeben) befindet sich in `data.decline_reason`.
envelope.voidedDer Umschlag wurde vom Absender vor vollständiger Unterzeichnung storniert. Unterscheidbar von `expired` (manuell vs. Timeout).
envelope.expiredDas Ablaufdatum des Umschlags ist verstrichen ohne vollständige Unterzeichnung. Die Liste der fehlenden Unterzeichner befindet sich in `data.missing_signers`.
recipient.signedEin einzelner Unterzeichner hat unterzeichnet (aber nicht unbedingt alle). Nützlich für sequenzielle Workflows: Auslösung für den Übergang zum nächsten Unterzeichner.
recipient.viewedEin Unterzeichner hat den Unterzeichnungslink geöffnet, ohne noch zu unterzeichnen. Nützlich für gezielte Nachverfolgungen.

Payload-Format

Alle Ereignisse folgen dem gleichen JSON-Schema auf oberster Ebene: `event`, `envelope_id`, `occurred_at`, `data`. Der Inhalt von `data` variiert je nach Ereignis (Felder sind in der API-Referenz pro Ereignis dokumentiert). Hier ist ein vollständiges `envelope.completed`-Beispiel.

{
  "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"
  }
}

Die Payload ist UTF-8 codiert, ohne BOM. Die HMAC-Signatur wird auf dem rohen Body berechnet, wie er gesendet wird — verändern Sie keine Leerzeichen, das erneute JSON-Parsing ändert häufig die Reihenfolge der Schlüssel und bricht die Verifizierung.

HMAC-Signatur verifizieren

Jede Anfrage wird mit Ihrem Webhook-Secret signiert (sichtbar nur einmal bei der Erstellung im Dashboard, dann verschlüsselt im Ruhezustand). Die Signatur wird im Header `Certyneo-Signature` im Format `t=<timestamp>,v1=<hex_hmac>` übertragen. Verifizieren Sie IMMER die Signatur, bevor Sie die Payload verarbeiten — ohne diesen Schritt kann jeder ein Ereignis fälschen und Ihren Endpoint aufrufen.

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)

Häufiger Fehler

Verwenden Sie NICHT `===` oder `==` zum Vergleichen der erwarteten Signatur mit der empfangenen. Nutzen Sie eine Timing-sichere Funktion (`crypto.timingSafeEqual` in Node, `hmac.compare_digest` in Python). Ohne dies offenbart die Zeitdifferenz beim Vergleich zwischen zwei Signaturen einem geduldigen Angreifer schrittweise das Secret (Timing Attack).

Retry-Richtlinie

Falls Ihr Endpoint etwas anderes als HTTP 2xx antwortet (Timeout, 5xx, Verbindung verweigert), wiederholen wir mit exponenziellem Backoff. Insgesamt 6 Versuche über ~9 Stunden. Nach 6 Versuchen wird das Ereignis in Ihrem Webhook-Dashboard als `failed` markiert und eine Benachrichtigungs-E-Mail wird gesendet.

VersuchKumulierte FristVerstrichene Zeit
#100
#2+ 1 min1 min
#3+ 5 min6 min
#4+ 30 min36 min
#5+ 2 h2h 36
#6+ 6 h8h 36

Wenn Sie ein fehlgeschlagenes Ereignis manuell erneut senden möchten, bietet das Webhook-Dashboard eine Schaltfläche « Re-deliver » für jede fehlgeschlagene Zustellung — verfügbar für 7 Tage nach dem endgültigen Fehler.

Testen ohne echte Envelope zu versenden

Der Endpunkt `/v1/webhooks/test` akzeptiert eine URL und einen Ereignistyp und sendet ein fiktives Payload, das genau wie ein echtes signiert ist. Ideal zur Validierung Ihres Handlers in Continuous Integration oder während der lokalen Entwicklung (mit ngrok oder ä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 Praktiken zu beachten

  • HMAC-Signatur VOR dem Lesen des Body überprüfen — Timing-sichere Vergleiche verwenden.
  • Jede `envelope_id` × `event` nur einmal verarbeiten, indem die bereits gesehenen IDs in der Datenbank gespeichert werden (Wiederholungen können dasselbe Ereignis erneut liefern).
  • HTTP 200 innerhalb von maximal 5 Sekunden antworten, dann asynchron verarbeiten (Queue). Andernfalls erhalten Sie einen Timeout und werden als Wiederholung gezählt.
  • Body brut + vollständige Signatur im Debug-Modus protokollieren — die HMAC-Verifizierung schlägt oft bei BOM oder unsichtbarem Whitespace fehl.
  • Dead-Letter-Queue für `failed`-Ereignisse verbinden, um manuell erneut zu liefern, falls auf Ihrer Seite ein Incident auftritt.
  • Eine Zeitabweichung von 5 Minuten zwischen `t=` und Empfangszeit tolerieren — darüber hinaus ablehnen, um Replays zu blockieren.

Weiterführende Informationen

Bereit, Ihre Systeme zu verbinden?

Erstellen Sie ein kostenloses Konto in 2 Minuten — die Webhook-Konfiguration ist bereits im Starter-Plan verfügbar (kostenlos, 5 Envelopes/Monat).