Webhook-Ereignisse
Vollständige Referenz für jeden Certivant-Webhook-Ereignistyp — Aufbau des Envelope-Schemas, Form der Nutzlast je Ereignis, Idempotenz-Muster und Reihenfolgeregeln.
Jede Webhook-Nutzlast hat dieselbe äußere Hülle. Nur der Block data ändert sich je Ereignistyp.
{
"event": "kyc.request.approved",
"requestId": "req_01J...",
"organizationId": "org_01H...",
"externalRef": "user-9281",
"subTenantName": "Acme Corp",
"subTenantExternalId": "cust_8842",
"status": "APPROVED",
"timestamp": "2026-05-27T10:04:21.000Z",
"data": { /* event-specific */ }
}
| Feld | Typ | Hinweise |
|---|---|---|
event | string | Der Ereignistyp. Beginnt immer mit kyc. |
requestId | string | Die Certivant-Anfrage-ID |
organizationId | string | Die Organisation, der die Anfrage gehört |
externalRef | string | null | Der Fremdschlüssel, den Sie beim Erstellen gesetzt haben |
subTenantName | string | null | Partnermodus: der bei der Anfrage gesetzte Anzeigename des Endkunden, falls vorhanden |
subTenantExternalId | string | null | Partnermodus: die bei der Anfrage gesetzte ID Ihres Endkunden — leiten Sie das Ereignis ohne Nachschlagen an den richtigen Kunden weiter |
status | enum | Aktueller Status der Anfrage zum Zeitpunkt des Ereignisses |
timestamp | string (ISO-8601) | Zeitpunkt, zu dem das Ereignis in die Warteschlange gestellt wurde |
data | object | null | Ereignisspezifische Nutzlast (siehe unten) |
Ereigniskatalog
kyc.webhook.test
Eine Verbindungsprüfung — wird gesendet, wenn Sie bei einer Webhook-URL auf Test klicken (organisationsweit unter KYC-Einstellungen → Entwickler oder pro Formular im Formulareditor). Es handelt sich nicht um ein echtes Verifizierungsereignis: Es enthält keine requestId und verändert auf Ihrer Seite nichts. Sein einziger Zweck ist, zu bestätigen, dass Ihr Endpunkt erreichbar ist.
Es enthält ein explizites test: true auf oberster Ebene, damit Ihr Handler direkt mit 2xx antworten kann — ohne eine Anfrage nachzuschlagen, den status zu prüfen oder sogar die Signatur zu verifizieren (es gibt nichts zu schützen, da ein Ping keinen Zustand verändert).
{
"event": "kyc.webhook.test",
"test": true,
"organizationId": "org_...",
"formId": "form_...", // present only for the per-form test
"formName": "KYC — Identity Verification",
"timestamp": "...",
"data": { "message": "This is a test webhook from your KYC platform." }
}
Empfohlener Handler:
const payload = JSON.parse(rawBody);
// Reachability ping — ack immediately, no auth needed.
if (payload.test === true || payload.event === "kyc.webhook.test") {
return new Response(JSON.stringify({ ok: true }), { status: 200 });
}
// …verify the signature and process real events below…
Echte Ergebnisereignisse (mit einem status aus dem Lebenszyklus-Enum) sollten Sie trotzdem auf ihre Signatur prüfen, bevor Sie darauf reagieren — siehe Signaturprüfung. Auch das Testereignis wird signiert (sofern ein Geheimnis gesetzt ist) — mit demselben Header X-Certivant-Signature-V2 wie echte Ereignisse und einem Unix-Zeitstempel in Sekunden. Sie können es also prüfen, müssen aber nicht.
Die Schaltfläche Testereignis senden im Dashboard kann außerdem eine Beispiel-Nutzlast für kyc.request.created, kyc.request.approved, kyc.request.rejected oder kyc.request.escalated senden. Beispiele haben die echte Envelope-Form, enthalten "test": true, verwenden eine erfundene requestId (req_sample_…) und data.sample: true und werden nie als echte Ereignisse gespeichert. Verarbeiten Sie sie im Produktivcode nicht: Verzweigen Sie zuerst auf payload.test === true.
kyc.request.created
Wird unmittelbar nach der Antwort 201 von POST /api/v1/kyc/requests ausgelöst. Nützlich, wenn Sie den Eingang bestätigen möchten, ohne auf die Einreichung durch den Antragsteller zu warten.
{
"event": "kyc.request.created",
"requestId": "req_...",
"organizationId": "org_...",
"externalRef": "user-9281",
"status": "PENDING_UPLOAD",
"timestamp": "...",
"data": null
}
kyc.request.processing
Wird in dem Moment ausgelöst, in dem die Anfrage in den Status PROCESSING übergeht — also wenn der Antragsteller (über eine gehostete Sitzung oder eine Formulareinreichung) oder Sie (über POST /requests/:id/submit) sie eingereicht haben. Dies ist das Signal „eingereicht“: Es kommt vor jeder Entscheidung, sodass Ihre Anwendung den Nutzer sofort nach Abschluss der Uploads in den Zustand „in Prüfung“ versetzen kann.
{
"event": "kyc.request.processing",
"requestId": "req_...",
"externalRef": "user-9281",
"status": "PROCESSING",
"timestamp": "...",
"data": null
}
Reagieren Sie darauf, wenn Sie zwischen der Einreichung und dem endgültigen Ereignis approved / rejected / escalated Fortschritt anzeigen möchten („Ihre Verifizierung wird geprüft“). Es enthält externalRef und die Felder des Unter-Mandanten, sodass Sie es ohne Nachschlagen dem richtigen Nutzer zuordnen können.
kyc.document.uploaded
Wird jedes Mal ausgelöst, wenn einer Anfrage ein Dokument hinzugefügt wird. Nützlich, wenn Ihre Anwendung eine Echtzeitanzeige „bisher erhaltene Dokumente“ zeigt.
{
"event": "kyc.document.uploaded",
"requestId": "req_...",
"status": "PENDING_UPLOAD",
"data": {
"documentId": "doc_...",
"documentType": "GOVERNMENT_ID",
"side": "front",
"mimeType": "image/jpeg",
"fileSize": 412938
}
}
kyc.check.completed
Wird ausgelöst, sobald jede einzelne Verifizierungsprüfung abgeschlossen ist. Eine einzige Anfrage kann mehrere davon erzeugen — eine für die Echtheit des Dokuments, eine für den Gesichtsabgleich, eine für die Lebenderkennung usw.
{
"event": "kyc.check.completed",
"requestId": "req_...",
"status": "PROCESSING",
"data": {
"checkType": "face_match",
"result": "pass",
"score": 0.91,
"details": { "method": "deep_face_match" }
}
}
Mögliche Werte für checkType: document_authenticity, ocr, face_match, liveness, sanctions_screening, pep_screening, adverse_media, proof_of_address, phone_verification, email_verification, registry_lookup (KYB).
kyc.request.approved
Das wichtigste Ereignis — die Anfrage ist genehmigt. Es wird sowohl ausgelöst, wenn die automatisierte Engine sie freigibt, als auch wenn ein menschlicher Prüfer eine eskalierte (oder zuvor automatisch abgelehnte) Anfrage im Dashboard oder über die API /decision manuell genehmigt. Bei manuellen Genehmigungen ist data.decision gleich "approve", und data.reason enthält die Notiz des Prüfers; die ausführlichen Engine-Felder (score, factors, …) sind nur bei automatisierten Entscheidungen vorhanden.
{
"event": "kyc.request.approved",
"requestId": "req_...",
"status": "APPROVED",
"data": {
"score": 94,
"decision": "approve",
"factors": [
{ "name": "document_authenticity", "weight": 0.3, "score": 0.96 },
{ "name": "face_match", "weight": 0.3, "score": 0.91 },
{ "name": "liveness", "weight": 0.2, "score": 0.98 },
{ "name": "sanctions_screening", "weight": 0.2, "score": 1.0 }
],
"document": {
"type": "GOVERNMENT_ID",
"issuingCountry": "GB",
"expiryDate": "2031-04-12",
"isExpired": false
},
"extracted": {
"fullName": "Alex Singh",
"dateOfBirth": "1988-11-03",
"documentNumber": "GBR-1234567",
"address": "1 Example Street, London",
"nationality": "GB"
},
"liveness": { "isLive": true, "confidence": 0.98 },
"faceMatch": { "isMatch": true, "confidence": 0.91 },
"sanctions": { "isMatch": false, "topScore": 0, "listsChecked": ["OFAC", "UN", "EU", "UK_HMT"] }
}
}
Der Block data.extracted ist Ihr wertvollster Teil — speichern Sie diese Werte am Nutzerdatensatz, damit Sie den Antragsteller nicht zweimal nach denselben Daten fragen.
kyc.request.rejected
Die Anfrage ist abgelehnt — entweder hat die automatisierte Engine sie abgelehnt (die Gründe finden Sie in data.disqualifiers und data.factors) oder ein menschlicher Prüfer hat eine eskalierte Anfrage manuell abgelehnt (data.decision = "reject", data.reason = die Notiz des Prüfers).
{
"event": "kyc.request.rejected",
"requestId": "req_...",
"status": "REJECTED",
"data": {
"score": 18,
"decision": "reject",
"disqualifiers": ["document_expired", "liveness_failed"],
"reviewTriggers": [],
"reason": "Document expired (2024-01-01) and liveness confidence below threshold (0.22).",
"factors": [
{ "name": "document_authenticity", "weight": 0.3, "score": 0.0, "verdict": "fail" },
{ "name": "liveness", "weight": 0.2, "score": 0.22, "verdict": "fail" }
],
"document": {
"type": "GOVERNMENT_ID",
"expiryDate": "2024-01-01",
"isExpired": true
},
"liveness": { "isLive": false, "confidence": 0.22 }
}
}
Sie können die Anfrage über POST /requests/:id/resubmit erneut einreichen, sofern Ihre Richtlinie dies zulässt.
kyc.request.escalated
Die Engine war nicht sicher — grenzwertige Ergebnisse, eine Teiltreffer-Sanktionsprüfung, ein Dokument von geringer Qualität oder etwas, das den Eskalationsschwellenwert Ihrer Organisation überschritten hat. Ein Mensch in Ihrer Organisation muss prüfen.
{
"event": "kyc.request.escalated",
"requestId": "req_...",
"status": "ESCALATED",
"data": {
"score": 62,
"decision": "escalate",
"reviewTriggers": ["sanctions_partial_match", "low_face_match_confidence"],
"factors": [...],
"sanctions": {
"isMatch": true,
"topScore": 0.72,
"listsChecked": ["OFAC", "UN", "EU", "UK_HMT"]
},
"faceMatch": { "isMatch": true, "confidence": 0.68 }
}
}
Ein Prüfer öffnet die Anfrage im Dashboard und entscheidet. Sobald er das getan hat, erhalten Sie einen zweiten Webhook — kyc.request.approved oder kyc.request.rejected — wobei die menschliche Entscheidung im Audit-Protokoll vermerkt ist.
kyc.request.cancelled
Wird ausgelöst, wenn eine Anfrage ausdrücklich abgebrochen wird (derzeit nur über das Dashboard — es gibt keinen API-Endpunkt).
{
"event": "kyc.request.cancelled",
"requestId": "req_...",
"status": "CANCELLED",
"data": { "reason": "applicant_withdrew", "cancelledBy": "user_..." }
}
Ereignisse der Live-Verifizierung — conference.*
Ereignisse der Live-ID-Verifizierung (von einer Person moderierte Live-Videositzung) verwenden einen eigenen, auf Konferenzen zugeschnittenen Aufbau — conferenceId, kycRequestId, applicantId, organizationId, Ihre externalRef, den status der Konferenz und Zeitstempel — anstelle des oben beschriebenen Envelopes kyc.request.*. Sie werden sowohl für Konferenzen ausgelöst, die im Dashboard geplant wurden, als auch für solche über die API.
| Ereignis | Wird ausgelöst, wenn |
|---|---|
conference.scheduled | Eine Live-Verifizierung erstellt wird. |
conference.rescheduled | Sie auf einen neuen Termin verschoben wird (previousScheduledFor). |
conference.cancelled | Sie abgesagt wird — über die API oder weil der Antragsteller die Einwilligung verweigert hat (cancellation). |
conference.started | Der Moderator die Sitzung startet (einmal pro Start). |
conference.completed | Der Moderator die Entscheidung erfasst (decision.decision, decision.outcome, Notizen, Aufzeichnungskennzeichen, Zertifikat). |
{
"event": "conference.completed",
"conferenceId": "cmf...",
"kycRequestId": "req_...",
"applicantId": "cma...",
"organizationId": "org_...",
"externalRef": "order-10042",
"status": "completed",
"decision": { "decision": "verified", "outcome": "approved", "requestStatus": "APPROVED", "notes": "..." },
"timestamp": "..."
}
Vollständige Nutzlast und die Planungs-API: Live-Verifizierungs-API.
Idempotenz
Certivant kann dasselbe Ereignis mehr als einmal zustellen (Netzwerkaussetzer, Wiederholungen — siehe Wiederholungen). Machen Sie Ihren Handler idempotent.
Der Header X-Certivant-Event-Id (wird auch als X-KYC-Event-Id gesendet) ist pro Ereignis eindeutig. Ein gängiges Muster:
async function handleWebhook(rawBody: string, headers: Headers) {
const eventId = headers.get("X-KYC-Event-Id")!;
// Skip if already processed
const seen = await db.webhookEvent.findUnique({ where: { id: eventId } });
if (seen) return;
const payload = JSON.parse(rawBody);
// Do the work + record the event in a single transaction
await db.$transaction([
db.webhookEvent.create({ data: { id: eventId, eventType: payload.event } }),
...yourBusinessLogic(payload),
]);
}
X-KYC-Event-Id bleibt über Wiederholungen hinweg gleich — dasselbe logische Ereignis behält dieselbe ID, unabhängig davon, wie viele Zustellversuche stattfinden.
Reihenfolge
Ereignisse für verschiedene Anfragen können in beliebiger Reihenfolge eintreffen. Ereignisse für dieselbe Anfrage werden in der Reihenfolge eingereiht, in der sie erzeugt wurden, aber Wiederholungen können sie verschachteln — etwa könnte ein hängengebliebenes Ereignis processing erst nach einem Ereignis approved für dieselbe Anfrage eintreffen.
Defensives Muster: Verwenden Sie immer das Feld status als maßgebliche Quelle und ignorieren Sie Ereignisse, deren status „älter“ ist als das, was Sie bereits gespeichert haben. Der Lebenszyklus verläuft streng vorwärts (PENDING_UPLOAD → PROCESSING → APPROVED/REJECTED/ESCALATED). Wenn Ihre Datenbank also APPROVED für die Anfrage ausweist und ein verspätetes Ereignis kyc.request.processing eintrifft, verwerfen Sie es.
Ereignis-Handler lokal testen
Zwei Schaltflächen im Dashboard helfen dabei:
- Test (neben jeder Webhook-URL, organisationsweit oder pro Formular) sendet einen Verbindungs-Ping
kyc.webhook.test(test: true). Verwenden Sie ihn, um zu bestätigen, dass Ihr Endpunkt erreichbar ist und mit2xxantwortet — er benötigt keine Anfragedaten und kein konfiguriertes Geheimnis. - Testereignis senden sendet eine vollständige Nutzlast
kyc.request.approvedmit Beispieldaten, damit Sie Ihren echten Pfad zur Verarbeitung von Ergebnissen durchspielen können.
Um andere Ereignistypen zu testen, schreiben Sie ein kleines Skript, das dieselbe Hülle erzeugt und per POST über einen Tunnel an Ihre Entwicklungs-URL sendet:
# tunnel localhost:3000 to a public URL
ngrok http 3000
# Then in the dashboard webhook settings, paste the ngrok URL,
# trigger real test requests via the API, and watch your logs.
Signaturprüfung
Prüfen Sie den Header X-Certivant-Signature-V2 (HMAC-SHA256 über timestamp.body, 300 s Toleranz), damit Sie den Webhook-Nutzlasten vertrauen können. Mit Beispielen für Node, Python, Go, Ruby und PHP.
Wiederholungen und Zustellung
Wie Certivant fehlgeschlagene Webhook-Zustellungen wiederholt — genauer Backoff-Zeitplan, Regel mit 10-Sekunden-Zeitlimit, Wiederherstellung erschöpfter Ereignisse und ein schneller Handler-Aufbau, um im Zeitbudget zu bleiben.