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 */ }
}
FeldTypHinweise
eventstringDer Ereignistyp. Beginnt immer mit kyc.
requestIdstringDie Certivant-Anfrage-ID
organizationIdstringDie Organisation, der die Anfrage gehört
externalRefstring | nullDer Fremdschlüssel, den Sie beim Erstellen gesetzt haben
subTenantNamestring | nullPartnermodus: der bei der Anfrage gesetzte Anzeigename des Endkunden, falls vorhanden
subTenantExternalIdstring | nullPartnermodus: die bei der Anfrage gesetzte ID Ihres Endkunden — leiten Sie das Ereignis ohne Nachschlagen an den richtigen Kunden weiter
statusenumAktueller Status der Anfrage zum Zeitpunkt des Ereignisses
timestampstring (ISO-8601)Zeitpunkt, zu dem das Ereignis in die Warteschlange gestellt wurde
dataobject | nullEreignisspezifische 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.

EreignisWird ausgelöst, wenn
conference.scheduledEine Live-Verifizierung erstellt wird.
conference.rescheduledSie auf einen neuen Termin verschoben wird (previousScheduledFor).
conference.cancelledSie abgesagt wird — über die API oder weil der Antragsteller die Einwilligung verweigert hat (cancellation).
conference.startedDer Moderator die Sitzung startet (einmal pro Start).
conference.completedDer 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 mit 2xx antwortet — er benötigt keine Anfragedaten und kein konfiguriertes Geheimnis.
  • Testereignis senden sendet eine vollständige Nutzlast kyc.request.approved mit 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.