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_type | Fires when |
|---|---|
identity.kyc.session.approved | The session passed every enabled module, or an operator approved it in review. |
identity.kyc.session.declined | The session failed a module, or an operator declined it in review. |
identity.kyc.session.review | The session is held for manual review (for example, an AML hit or a low-confidence check). |
identity.kyc.session.expired | The 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 status | application.decision | event_type fired | Payload |
|---|---|---|---|
completed | approved | identity.verification.completed | Full envelope |
sdk_callback_received | approved | identity.verification.completed | Full envelope |
kyc_failed | rejected | identity.verification.failed | Full envelope |
manual_review | manual_review | identity.application.manual_review | Full 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
.reviewlands before.approved. Comparecreated_at, or re-read the current decision fromGET /v3/session/<id>/decision/when an event looks stale.