Reference

Events

The KYC session and application event types, and exactly when each fires.

Two families of events reach your webhook endpoint: ThirdFactor Verify session events and application workflow events. Route on the event_type field, which delivery storage adds to every payload. This page lists every event_type in use today, a full example payload for each, and exactly which status transition produces it.

Info

Delivery is at-least-once and best-effort from the application/session side — see Webhooks overview for the retry schedule and idempotency guidance. This page is scoped to which events exist and when they fire.

Note

Looking for finer-grained events — per-module results, AML hits, review-case movement, session created/started, credit alerts? Those ship through Connect endpoints with their own event keys and envelope — see the Connect event catalog. This page covers the classic tenant webhook's event families only.

KYC session events

Terminal ThirdFactor Verify sessions (/v3) emit the flat session payload with one of these event_type values:

event_typeFires when
identity.kyc.session.approvedThe session passed every enabled module, or an operator approved it in review.
identity.kyc.session.declinedThe session failed a module, or an operator declined it in review.
identity.kyc.session.reviewThe session is held for manual review (for example, an AML hit or a low-confidence check).
identity.kyc.session.expiredThe hosted URL lapsed before the applicant reached a decision.

The payload is flat — branch on the top-level decision (mirrored by status) and identify the user by vendor_data.

{
  "session_id": "6f0e2b7a-1234-4a11-9c22-0a5f7d1e9b30",
  "status": "approved",
  "decision": "approved",
  "decision_reason": "",
  "vendor_data": "your-user-reference-123",
  "workflow_id": "9c1f4b2e-88aa-4a0d-9e11-2c7b6a0f5d40",
  "application_id": null,
  "created_at": "2026-07-03T08:00:00Z",
  "completed_at": "2026-07-03T08:06:12Z",
  "event_type": "identity.kyc.session.approved",
  "idempotency_key": "b2a1c9d0-6e5f-4a3b-8c7d-1e2f3a4b5c6d"
}

Warning

.review is not terminal in the business sense. Expect a later .approved or .declined once an operator resolves the session in Console → Verifications. Model your state machine so a session can sit in review for hours and still settle. AML fails closed — if the screening service is unavailable, sessions land in review rather than passing silently.

Application events

Applications (/api/v1) emit an event when they reach one of three terminal (or terminal-for-now) states. The legacy event string differs by outcome — this is a common source of confusion, so treat the table below, not a naming pattern, as the source of truth:

Application statusapplication.decisionevent_type firedPayload
completedapprovedidentity.verification.completedFull envelope
sdk_callback_receivedapprovedidentity.verification.completedFull envelope
kyc_failedrejectedidentity.verification.failedFull envelope
manual_reviewmanual_reviewidentity.application.manual_reviewFull envelope, blocklist_match populated when the trigger was a risk-list hit

Warning

This is exactly why the API reference and this doc both say branch on application.decision, not on the event / event_type string: an approved application's event is named identity.verification.completed, not identity.application.approved. Pattern-matching event_type.startsWith("identity.application.") will silently miss approvals and rejections.

{
  "payload_version": "2026-05-17",
  "event": "identity.verification.completed",
  "application": {
    "id": "ge9vgmlmfbi9k5q",
    "external_ref": "core-banking-id-123",
    "tracking_id": "APP-2026-0001",
    "status": "completed",
    "decision": "approved",
    "flow_id": "savings_v1",
    "current_step_id": "done",
    "created_at": "2026-05-18T08:00:00Z",
    "updated_at": "2026-05-18T08:12:40Z"
  },
  "form_data": {
    "full_name": "Aarav Sharma",
    "phone": "9841000000",
    "account_type": "savings"
  },
  "product": { "id": "savings-standard", "name": "Standard Savings" },
  "sdk_response": {
    "is_verified": true,
    "percentage_match": 96.4,
    "document_type": "citizenship"
  },
  "blocklist_match": null,
  "created_at": "2026-05-18T08:12:40Z",
  "event_type": "identity.verification.completed",
  "idempotency_key": "f6e5a3b4-a293-4e7f-a0b1-5c6d7e8f9012"
}

Note

Legacy event names still appear for backwards compatibility. New integrations should key off application.decision rather than pattern-matching event names.

Statuses that do not currently fire a webhook

Not every status transition is wired to a delivery. If you're waiting on an event that never arrives, check whether the status you're watching is on this list before assuming a delivery failure:

Status (session or application)Fires an event?
not_started, in_progress, awaiting_user, resubmitted (session)No — not terminal
draft, awaiting_form, form_started, form_filled, scheduled, sdk_redirected, visit_branch (application)No — not terminal
abandoned (session or application)No classic event fires. A Connect endpoint subscribed to session.abandoned does receive it — see Integrations. Otherwise poll GET /v3/session/ / GET /api/v1/applications/ with a status filter.
kyc_expired (session)No event fires; distinct from expired (hosted-URL TTL), which does fire identity.kyc.session.expired.

Tip

When you receive a status/decision you do not recognise, treat it as in_review — hold the user in a pending state and wait for a terminal event. That is what the SDKs do, deliberately.

Flow-spawned sessions emit both

A ThirdFactor Verify session created by a hosted flow's identity step is linked to a parent application. When it settles it emits its own identity.kyc.session.* event and advances the parent application, which emits the usual application event from the table above. In that case the session payload's application_id is set.

If you subscribe to both families, deduplicate at the application level so you don't process the same customer twice — the two events have different idempotency_key values because they're genuinely different deliveries. For pure /v3 sessions there is no parent application, application_id is null, and only the identity.kyc.session.* event fires.

Retries and ordering

Delivery is at-least-once, which has two consequences:

  • Events can arrive more than once. Deduplicate on idempotency_key. See Webhooks overview for the exact backoff schedule.
  • Events can arrive out of order. Do not assume .review lands before .approved. Compare created_at, or re-read the current decision from GET /v3/session/<id>/decision/ when an event looks stale.

FAQ