Webhooks — Empfangen Sie Signaturedeignisse in Echtzeit
Konfigurieren Sie eine HTTPS-URL in Ihrem Certyneo-Dashboard und empfangen Sie einen HMAC-SHA256-signierten POST, sobald ein Ereignis in Ihren Umschlägen auftritt: Unterzeichnung, Ablehnung, Ablauf. 11 unterstützte Ereignisse, 5 Zustellungsversuche mit exponentiellem Backoff, kryptographische Verifizierung in 8 Codezeilen.
< 5s
Durchschnittliche Lieferfrist nach dem Ereignis
5x
Zustellungsversuche insgesamt, verteilt über etwa 1 h 20
HMAC-SHA256
Signaturalgorithmus für jede Anfrage
Ereigniskatalog
Die 12 folgenden Ereignisse decken den gesamten Lebenszyklus eines Certyneo-Umschlags ab. Aktivieren Sie die für Sie relevanten in Einstellungen → Webhooks, ignorieren Sie die anderen — das Abonnement ist granular pro Ereignis.
| Ereignis | Auslöser |
|---|---|
envelope.created | Eine Umschlag wird erstellt (über UI, API oder Vorlage) — nützlich, um einen CRM-Datensatz bei der Erstellung zu synchronisieren. |
envelope.sent | Der Umschlag wird an die Unterzeichner gesendet (erste E-Mail gesendet). Markiert den Beginn des aktiven Unterzeichnungszyklus. |
envelope.completed | Alle Unterzeichner haben unterzeichnet und das versiegelte eIDAS-PDF ist gespeichert. Die Payload enthält signedDocumentUrl, einen vorsignierten Link, der 7 Tage gültig ist; andernfalls GET /v1/envelopes/id/signed-document und die Audit-Trail über GET /v1/envelopes/id/audit-trail. |
envelope.declined | Ein Unterzeichner hat den Umschlag abgelehnt. Die Adresse des Ablehnenden befindet sich in `data.declinedBy` und der Grund, falls eingegeben, in `data.reason`. |
envelope.voided | Der Umschlag wurde vom Absender vor vollständiger Unterzeichnung storniert. Unterscheidbar von `expired` (manuell vs. Timeout). |
envelope.expired | Das Ablaufdatum des Umschlags ist verstrichen, ohne dass eine vollständige Unterzeichnung vorliegt. Fragen Sie GET /v1/envelopes/id ab, um fehlende Unterzeichner zu ermitteln. |
envelope.returned_to_sender | Ein Unterzeichner hat den Umschlag zur Korrektur an den Absender zurückgesendet, ohne ihn abzulehnen. Der Grund befindet sich in `data.reason` und der Autor der Rückgabe in `data.returnedBy`. |
envelope.resubmitted | Der Aussteller hat einen zuvor zurückgegebenen Umschlag korrigiert und erneut eingereicht. Markiert die Wiederaufnahme des Signaturzyklus. |
recipient.signed | Ein einzelner Unterzeichner hat unterzeichnet (aber nicht unbedingt alle). Nützlich zum Verfolgen des Fortschritts und zum Übergehen zum nächsten Schritt eines sequenziellen Workflows. Achtung: Das versiegelte PDF existiert zu diesem Zeitpunkt noch nicht, auch nicht für den letzten Unterzeichner — ein hier gestarteter Download gibt HTTP 409 zurück. Verwenden Sie envelope.completed für das Dokument. |
recipient.viewed | Ein Unterzeichner hat den Unterzeichnungslink geöffnet, ohne noch zu unterzeichnen. Nützlich für gezielte Nachverfolgungen. |
recipient.approved | Ein Genehmiger hat den Umschlag validiert, ohne eine Signatur anzubringen (interner Validierungs-Workflow). Die Adresse befindet sich in `data.approvedBy`. |
recipient.bounced | Der Messaging-Server eines Empfängers hat die Einladung oder Wiederholung endgültig abgelehnt (nicht vorhandenes Postfach, inaktive Domain). Der Empfänger erhält den Status BOUNCED und die automatischen Wiederholungen werden gestoppt. Die Adresse befindet sich in `data.recipientEmail`: Korrigieren Sie sie und senden Sie den Umschlag erneut. |
recipient.signed bedeutet nicht «Dokument verfügbar»
recipient.signed wird für JEDEN Unterzeichner ausgegeben, wenn dieser seine Signatur abgeschlossen hat — einschließlich des letzten, bevor das versiegelte PDF zusammengestellt und gespeichert wird. Ein Download, der von diesem Handler ausgelöst wird, erhält daher immer eine HTTP 409 «Signed document not available until the envelope is COMPLETED». Das ist kein Fehler: es ist ein «noch nicht bereit». Abonnieren Sie envelope.completed, um das Dokument abzurufen, und verwenden Sie recipient.signed, um den Fortschritt zu verfolgen (wer hat unterschrieben und wann).
Payload-Format
Alle Zustellungen teilen das gleiche JSON-Schema auf oberster Ebene: `event`, `data` und `timestamp`. Der Ereignisname wird auch im Header `X-Certyneo-Event` wiederholt, was das Routing ermöglicht, bevor der Body geparst wird. Der Inhalt von `data` variiert je nach Ereignis, bleibt aber immer ein flaches Objekt mit einfachen Werten — niemals ein Array oder verschachteltes Objekt. Hier ist eine vollständige `envelope.completed` Zustellung.
POST /webhooks/certyneo HTTP/1.1
Host: your.app
Content-Type: application/json
X-Certyneo-Event: envelope.completed
X-Certyneo-Delivery-Id: cm8f2h6a10004qr9k5p2wm3xt
X-Certyneo-Signature: 4f3d1c8b2a9e7f60d5c4b3a2918e7f6d5c4b3a2918e7f6d5c4b3a2918e7f6d5c{
"id": "cm8f2h6a10004qr9k5p2wm3xt",
"event": "envelope.completed",
"data": {
"envelopeId": "cm7x2k9p40001qz8h3f7bn2ld",
"sandbox": false,
"subject": "Contrat de prestation Acme Corp",
"status": "COMPLETED",
"signatureLevel": "ADVANCED",
"aesSignerCount": 2,
"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 ist ein vorsignierter Link, der 7 Tage lang gültig ist, gerechnet ab dem Ereignis: Er vermeidet einen zweiten authentifizierten Aufruf zum Abrufen des PDF. Er ist abwesend — und nicht null — wenn die Vorsignatur fehlgeschlagen ist oder bei einem QES-Umschlag ohne gespeichertes Dokument. Verwenden Sie dann GET /v1/envelopes/id/signed-document, das bleibt die maßgebliche Quelle. Achten Sie auch auf manuelle Wiederholung aus der Dead-Letter-Queue mehr als 7 Tage nach dem Ereignis: Der Link im Payload ist abgelaufen, der API-Endpoint nicht.
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 (wird bei der Erstellung des Abonnements nur einmal angezeigt). Die Signatur wird im Header `X-Certyneo-Signature` übermittelt: Dies ist der HMAC-SHA256 des rohen Request-Body, hexadezimal kodiert, ohne Präfix oder Zeitstempel. IMMER die Signatur vor der Verarbeitung des Payloads überprüfen — 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 {
// 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)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
Wenn Ihr Endpoint zu lange zum Antworten braucht, die Verbindung ablehnt oder einen 5xx-Code (oder 429) zurückgibt, versuchen wir es nach einem exponentiellen Backoff erneut: 5 Versuche insgesamt über etwa 1 h 20. Nach dem fünften Versuch wird das Ereignis in eine Dead-Letter-Queue verschoben – sie ist einsehbar und kann von Ihrem Dashboard aus erneut abgespielt werden, wird aber nicht mehr automatisch erneut versucht. Eine ausdrückliche Ablehnung (401, 403, 404, 410, 422…) wird hingegen nie erneut versucht: die Antwort würde sich nicht ändern, das Ereignis geht direkt in die Dead-Letter-Queue.
| Versuch | Verzögerung vor dem Versuch | Seit dem Ereignis verstrichene Zeit |
|---|---|---|
| #1 | 0 | 0 |
| #2 | + 1 min | 1 min |
| #3 | + 5 min | 6 min |
| #4 | + 15 min | 21 min |
| #5 | + 1 h | 1 h 21 |
Die angegebenen Verzögerungen sind Mindestwerte: Der Retry-Scan wird durch einen Cron-Job zeitgesteuert, daher kann ein Versuch leicht nach der theoretischen Zeit gestartet werden. Aufgegebene Ereignisse werden in Webhooks → Fehler aufgelistet, mit einer manuellen Replay-Schaltfläche und ohne Aufbewahrungsfristbeschränkung. Ein Endpoint, der fünf endgültige Fehler hintereinander hat – oder der 404 / 410 antwortet – wird automatisch deaktiviert, und Sie werden per E-Mail benachrichtigt: reaktivieren Sie ihn nach der Korrektur, der Zähler setzt sich auf Null zurück (jede erfolgreiche Lieferung setzt ihn auch auf Null zurück).
Testen ohne echte Envelope zu versenden
Erstellen Sie zunächst ein Abonnement — die Antwort enthält das `secret`, das für die HMAC-Verifizierung erforderlich ist und nur einmal angezeigt wird. Über Einstellungen → Webhooks sendet die Schaltfläche «Testen» dann einen signierten POST genauso wie eine echte Zustellung und zeigt Ihnen die rohe Antwort Ihres Servers an: Das ist die schnellste Möglichkeit, Ihren Handler lokal über ngrok oder in einer Continuous-Integration zu validieren.
# 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"]
}'Die URL muss öffentlich über HTTPS erreichbar sein: Eine private Adresse oder localhost wird durch den SSRF-Schutz zum Zeitpunkt der Abonnementserstellung abgelehnt.
Abonnements sind wie die Schlüssel nach Umgebung getrennt: Ein mit einem sk_test_-Schlüssel erstellter Webhook empfängt nur die Ereignisse von Testumschlägen, ein mit einem sk_live_-Schlüssel erstellter nur die echter Umschläge. Ein Testumschlag erreicht Ihre Produktions-URL also nie, und jede Payload gibt „sandbox“ an (true oder false).
6 Praktiken zu beachten
- HMAC-Signatur VOR dem Lesen des Body überprüfen — Timing-sichere Vergleiche verwenden.
- Deduplizieren Sie auf dem Header `X-Certyneo-Delivery-Id` (identisch mit `id` im Body), indem Sie bereits gesehene Identifikatoren in der Datenbank speichern — eine Wiederholung kann ein Ereignis erneut zustellen, das Sie bereits bearbeitet haben, wenn Ihr 2xx verloren ging, mit demselben Identifikator bei jedem Versuch.
- HTTP 2xx in maximal 10 Sekunden antworten, dann asynchron verarbeiten (Queue). Danach wird die Zustellung unterbrochen und als Fehler gezählt.
- Body brut + vollständige Signatur im Debug-Modus protokollieren — die HMAC-Verifizierung schlägt oft bei BOM oder unsichtbarem Whitespace fehl.
- Überwachen Sie die Seite Webhooks → Fehler: Sie werden per E-Mail benachrichtigt, wenn Ihr Endpoint deaktiviert wird, aber nicht ereignisspezifisch – wenn ein Ereignis seine Versuche ausschöpft, müssen Sie es selbst erneut abspielen.
- Filtern Sie Ereignisse bei der Anmeldung statt in Ihrem Handler und bleiben Sie unter der Grenze von 5 Abos pro Konto.
Sie sind nicht verpflichtet, einen Endpunkt zum Empfang dieser Ereignisse zu hosten: Der Connector richtet das Abonnement für Sie ein und startet den Workflow direkt bei einem Umschlag-Ereignis. Siehe Certyneo für Power Automate und Microsoft 365.
Weitere Informationen
Bereit, Ihre Systeme zu verbinden?
Webhooks und die REST-API sind ab dem Standard-Plan enthalten. Erstellen Sie Ihr Konto, generieren Sie einen `sk_test_`-Schlüssel und verbinden Sie Ihren Endpoint in der Sandbox, bevor Sie in die Produktion gehen.