Integrieren
Webhooks
Signierte Ereignisse mit Replay-Schutz, exponentiellem Backoff und einem Zustellprotokoll. Die Prüfung ist fünf Zeilen — und sie ist nicht optional.
Die Signatur prüfen
Jede Zustellung trägt Signido-Signature in der Form t=<unix>,v1=<hex>. Der MAC ist HMAC-SHA256 über die UTF-8-Bytes von "<t>.<roher Body>", mit dem Secret des Endpunkts als Schlüssel.
- Der Zeitstempel liegt im MAC. Wird nur der Body signiert, bleibt die Signatur für immer gültig, und wer eine Anfrage einmal mitgeschnitten hat, kann sie ein Jahr später erneut senden. Weil
tmitsigniert ist, erkennt ein Empfänger einen Replay ohne jeden gespeicherten Zustand. - Der MAC gilt für die rohen Bytes. Wer JSON parst und neu serialisiert, berechnet den MAC über eine andere Zeichenkette — Schlüsselreihenfolge, Leerzeichen und Zahlenformat unterscheiden sich. Das Ergebnis sieht wie ein Angriff aus und ist ein Bug. Erst prüfen, dann parsen.
- Vergleichen Sie in konstanter Zeit. Ein Vergleich, der beim ersten Unterschied abbricht, verrät über seine Laufzeit, wie viele führende Zeichen gestimmt haben.
hmac.compare_digest,crypto.timingSafeEqualoderverifyWebhookSignatureaus dem SDK. - Das Schema ist versioniert. Lesen Sie das
v1-Element aus der Liste und ignorieren Sie unbekannte Elemente, damit ein später hinzukommendesv2Ihren Empfänger nicht bricht.
import { parseWebhookEvent } from '@signido/sdk';
export async function POST(request: Request): Promise<Response> {
// Der ROHE Body. Nicht erst parsen und wieder serialisieren —
// die Signatur gilt für genau diese Bytes.
const payload = await request.text();
try {
const { event } = await parseWebhookEvent({
payload,
signatureHeader: request.headers.get('signido-signature'),
secret: process.env.SIGNIDO_WEBHOOK_SECRET,
});
// Die Event-ID ist Ihr Idempotenzschlüssel: dasselbe Ereignis nach einem
// Timeout erneut zugestellt trägt dieselbe ID.
if (await alreadyProcessed(event.id)) return new Response(null, { status: 200 });
switch (event.type) {
case 'request.completed':
await fileCompletedAgreement(event.data);
break;
default:
break;
}
await remember(event.id);
return new Response(null, { status: 200 });
} catch {
// Nicht 500: eine ungültige Signatur ist keine vorübergehende Störung,
// und ein 5xx würde acht Wiederholungsversuche auslösen.
return new Response(null, { status: 400 });
}
}Header
- Signido-Signature
- t=<unix Sekunden>,v1=<hex HMAC-SHA256>. Das Einzige, was zählt.
- Signido-Event-Id
- Ihr Idempotenzschlüssel. Dasselbe Ereignis nach einem Timeout erneut zugestellt trägt dieselbe ID; wer sie speichert, erkennt das Duplikat. Identisch mit "id" im Body.
- Signido-Event-Type
- Der Ereignistyp, auch im Body als "type".
- Signido-Delivery-Id
- Diese eine Zustellung. Bei einem erneuten Senden ein neuer Wert.
- Signido-Attempt
- Der Versuch, beginnend bei 1.
- Signido-Timestamp
- Derselbe Wert wie das t im Signatur-Header, zur Bequemlichkeit.
Der Body
Vier Schlüssel auf der obersten Ebene, snake_case innerhalb von data. Der Body enthält Kennungen, Zustände, Digests und Zeitstempel — keine Namen, keine E-Mail-Adressen, keine Feldwerte, keinen Dokumentinhalt. Ein Webhook-Body liegt in Ihren Logs und in unseren; alles Personenbezogene ist über die authentifizierte API unter den Kennungen erreichbar, die hier stehen.
{
"id": "request.completed:req_01JQ8ZK9YV3X2M4N5P6Q7R8S9T",
"type": "request.completed",
"created": "2026-07-30T09:15:00.000Z",
"test_mode": false,
"data": {
"request_id": "req_01JQ8ZK9YV3X2M4N5P6Q7R8S9T",
"status": "COMPLETED",
"completed_at": "2026-07-30T09:14:58.412Z",
"requested_signature_level": "SES",
"documents": [
{ "document_id": "doc_01JQ8ZK…", "sha256": "9f2c…", "page_count": 4 }
],
"document_sha256": "9f2c…",
"certificate": { "certificate_id": "coc_01JQ8ZK…" }
}
}test_mode: true steht für eine Zustellung von einem Endpunkt der Test-Umgebung. Ein erneut gesendetes Ereignis trägt zusätzlich redelivery_of mit der ID des Originals — und eine neue id, damit ein Empfänger, der IDs speichert, den Nachversand nicht als Duplikat verwirft.
Ereignisse
Ein Endpunkt abonniert eine Liste von Typen. Ein unbekannter Name wird beim Anlegen abgelehnt statt gespeichert: ein Tippfehler wie request.complete würde sonst zu einem Endpunkt führen, der konfiguriert, aktiv, grün im Dashboard und für immer still ist.
| Typ | Wann | Status |
|---|---|---|
| document.created | Ein Dokument existiert; die kanonische PDF wird vorbereitet. | noch nicht produziert |
| request.sent | Der Auftrag hat den Entwurf verlassen, die Einladungen sind unterwegs. | noch nicht produziert |
| request.viewed | Die Vereinbarung selbst wurde geöffnet, nicht nur der Link. | noch nicht produziert |
| request.completed | Alle nötigen Signaturen liegen vor; die Nachweise sind erzeugt. | wird ausgeliefert |
| request.declined | Eine Person hat abgelehnt. | noch nicht produziert |
| request.expired | Die Frist ist verstrichen. | noch nicht produziert |
| signer.notified | Eine Einladung oder Erinnerung wurde versandt. | noch nicht produziert |
| signer.opened | Der Signaturlink wurde aufgelöst. | noch nicht produziert |
| signer.authenticated | Die Identifikation ist bestanden. | noch nicht produziert |
| signer.signed | Eine Person hat unterschrieben. | noch nicht produziert |
| invoice.created | Eine Rechnung wurde erstellt. | noch nicht produziert |
| payment.paid | Eine Zahlung ist eingegangen. | noch nicht produziert |
| subscription.updated | Das Abonnement hat sich geändert. | noch nicht produziert |
Zustellung, Wiederholung, Abschaltung
- Erfolg ist 2xx. Alles andere ist ein Fehlschlag. Zeitüberschreitung nach 10 Sekunden.
- Acht Versuche mit exponentiell wachsendem Abstand über rund 15 Stunden. Ein
4xxaußer408,425und429gilt als endgültig und wird nicht wiederholt: es ist die Aussage „diese Anfrage ist falsch“, und sie sieben weitere Male unverändert zu senden hieße, nicht zuzuhören. - Nur HTTPS, keine Zugangsdaten in der URL, keine Weiterleitungen, keine privaten oder link-lokalen Adressen.
- Ein dauerhaft ausfallender Endpunkt wird abgeschaltet — mit Begründung, und die noch wartenden Zustellungen werden abgebrochen statt weiter versucht. Eine URL achtzig Mal hintereinander erfolglos aufzurufen ist der Weg, auf dem ein stillgelegter Host eines Kunden in einem Abuse-Report landet.
- Jeder Versuch steht im Protokoll: Status, Laufzeit, gekürzter Antwort-Body. Ein Ereignis kann von dort aus erneut gesendet werden.