Skip to main content

Overview

Webhooks are how Didit pushes real-time updates to your backend whenever a verification session, business session, vendor user, vendor business, or transaction changes state. Subscribing one or more destinations to the events you care about lets you react the moment a result is ready — usually within seconds of the verifying user finishing their flow. Webhooks are the recommended integration pattern. Polling GET /v3/session/{id}/decision/ is supported as a fallback, but it is slower, costs more requests, and skips events like data.updated, transaction.status.updated, and entity-level changes that are only emitted through webhooks. Treat webhook delivery as the source of truth and only poll on cold start or for reconciliation.
Cloudflare / WAF users. Didit delivers webhooks from the static IP 18.203.201.92 with User-Agent: DiditWebhook/2.0 +https://didit.me. If your edge blocks unknown clients, allow this IP in Security > WAF > Tools > IP Access Rules and choose Allow for the receiving hostname.

Event types

Use the exact value in a destination’s subscribed_events array. There is no wildcard — list every family you want to receive — and you can spread events across multiple destinations. These ten values are the complete set of subscribable events - there is no session.status.updated, kyc.completed, or similar. The only other delivery Didit makes is the unsigned kyb.registry_search.resolved callback, which goes to the per-request webhook_url you pass to POST /v3/kyb/search/, never to a destination.
The Business Console > API & Webhooks > Try Webhook menu sends fully-formed payloads for every one of these events (approved, declined, in-review, Unicode names, KYC/KYB, entity, activity, and transaction). Use it to validate your endpoint before going live.

Payload shape

Envelope

Every webhook shares the same envelope. Fields are always serialised with sort_keys=True and compact separators (, / :) so the same payload reproduces the same signature on both sides. For session events (status.updated, data.updated on regular and business sessions) the envelope additionally carries:

Session envelope example

The decision object is the same schema returned by GET /v3/session/{id}/decision/. For the canonical per-feature field reference (every property on id_verifications[], aml_screenings[], etc.) see Data models. We deliberately do not duplicate the per-feature field tables here so the schema only lives in one place.

V3 plural arrays (important)

Inside decision, every per-feature result is a plural array so a single workflow can include several instances of the same feature (for example, multiple ID checks across documents):
Each item carries a node_id matching the workflow graph plus a per-feature status. Do not read the legacy V2 singular fields (id_verification, nfc, liveness, face_match, phone, email, poa, aml, ip_analysis, database_validation); they only appear if a destination is explicitly pinned to webhook_version: "v2". Document-collection features — KYB documents and Document AI — do not have a dedicated plural array; they appear in the decision features[] list as { "feature": "DOCUMENT_AI", "node_id": "…" } (one entry per node). See Document AI.

Entity event payloads

Entity statuses are ACTIVE, FLAGGED, or BLOCKED. user.data.updated and business.data.updated keep the status field and add a changed_fields array plus a changes map of { field: { previous, current } }. business.* events use vendor_business_id instead of vendor_user_id.

Transaction event payloads

Transaction webhooks ride on the same fan-out infrastructure and use the same envelope, but identify the transaction instead of a session:
New transactions default to APPROVED. If a rule, blocklist, or workflow changes the status during creation, the initial transaction.created payload reflects that final status. transaction.status.updated carries the same shape with the updated status. Transaction statuses are APPROVED, IN_REVIEW, DECLINED, or AWAITING_USER; severity is UNKNOWN / LOW / MEDIUM / HIGH / CRITICAL and direction is INBOUND / OUTBOUND. Transaction webhooks are only delivered for live applications — sandbox transactions never fan out.

KYB registry search callback

The asynchronous POST /v3/kyb/search/ flow delivers one extra payload that is not a destination event: when you pass a webhook_url to the search request, Didit POSTs a kyb.registry_search.resolved callback to that URL once the registry resolves. It differs from destination webhooks in three ways:
  • It is unsigned — no X-Signature* headers. Didit marks it with X-Didit-Unsigned-Callback: true instead; validate it by matching request_id against the id returned by the search request.
  • The event name lives in event_type (not webhook_type), and there is no application_id or environment.
  • timestamp and created_at are epoch integers, like other webhooks but unlike the search endpoint’s ISO created_at.
The callback follows the same retry policy as destination webhooks (retries re-stamp timestamp and stay unsigned). See the KYB Registry API for the full candidate-list schema.

Signature verification

Every webhook is signed with HMAC-SHA256 using the destination’s secret_shared_key. Three signature headers are sent so you can pick the one that survives your stack: Headers Didit always sends:

Why three variants?

Some web frameworks (Express body parsers, Django middleware, API gateways) silently re-encode JSON before your handler reads it. If the original body contained "José" and your middleware rewrites it to "José", the bytes change even though the data is identical. X-Signature then fails. X-Signature-V2 is computed from a canonical JSON form (sort keys, compact separators, unescaped Unicode) that almost every middleware reproduces deterministically — and X-Signature-Simple falls back to signing only a small, parser-independent string when even that fails.
  1. Try X-Signature-V2 first. Re-encode JSON.parse(body) with sorted keys and Unicode preserved, then HMAC-SHA256(secret, canonical) and timingSafeEqual against the header.
  2. If V2 fails and you can read raw bytes, try X-Signature against HMAC-SHA256(secret, rawBody).
  3. If both fail, fall back to X-Signature-Simple against HMAC-SHA256(secret, "{timestamp}:{session_id}:{status}:{webhook_type}") — and treat any decision data as untrusted unless you can re-fetch it from the API.
In all cases, reject any request older than 5 minutes (abs(now - X-Timestamp) > 300) to defend against replays.

Retry policy

If your endpoint responds with a 5xx or 404, Didit retries up to 2 times with exponential backoff:
  • 1st retry — about 1 minute after the initial failure.
  • 2nd retry — about 4 minutes after the 1st retry.
After the second retry the delivery is dropped. Each delivery attempt (initial + retries) is logged as a separate entry in the Business Console under the destination’s Deliveries tab, so you can replay or inspect any attempt. Other delivery rules to know:
  • The outbound HTTP request times out after 5 seconds.
  • Didit blocks deliveries to private / localhost URLs as an SSRF guard and never follows redirects. Use a public HTTPS endpoint that answers directly.
  • 2xx is treated as success. Timeouts and connection failures count as retryable (they surface as 504/503 in the delivery log); 3xx and 4xx responses other than 404 are not retried.
  • On retry Didit recomputes signatures with a fresh X-Timestamp, so the timestamp/signature pair always lines up.

Setting up a destination

You can manage destinations from the Business Console > API & Webhooks or via the Management API.
1

Create the destination

Call POST /v3/webhook/destinations/ (or click Add destination in the console) with a label, public url, webhook_version ("v3"), and a subscribed_events array containing every event family this endpoint should receive. At least one event is required and no wildcard exists.
2

Store the secret_shared_key

The create response returns the destination’s secret_shared_key. Store it now — it is the only secret you will ever see for this destination, scoped to this destination, and is the input to all three HMAC-SHA256 signature variants.
3

Implement the endpoint

Expose a public HTTPS POST route, read the raw body, verify the signature (preferring X-Signature-V2), check the timestamp window, then dispatch on webhook_type. Return 2xx as soon as you have queued the work — do heavy processing asynchronously.
4

Confirm receipts

Watch the Deliveries tab on the destination. Healthy webhooks return 200 within a second or two. 5xx/404 triggers Didit’s retry policy; persistent failures are dropped after the second retry.
5

Iterate with Try Webhook

Use the Try Webhook console scenarios to drive your endpoint through approved, declined, in-review, KYB, entity, activity, and transaction payloads before flipping production traffic on.
Didit webhook testing feature in the Business Console

Send test webhooks directly from the Business Console to validate your endpoint before going live.

You can create as many destinations as you need. A common pattern is one destination per consumer: e.g. KYC events → your auth service, KYB events → your compliance ops service, transactions → your fraud queue. Each destination has its own secret, version, and subscription list, so a rotated secret or a bad deploy on one consumer does not affect the others.

Code samples

To ensure the security of your webhook endpoint, verify the authenticity of incoming requests using the destination’s secret_shared_key.
Use X-Signature-V2. It is the variant most resilient to middleware re-encoding, and it fully authenticates the body (including decision).
If you can read the raw bytes before any parser touches them, the original X-Signature variant works the same way but signs the raw body. We keep it in the legacy section below for completeness.

Testing

You do not need to run a full verification to exercise your endpoint.
  • From Business Console > API & Webhooks > Try Webhook, pick a scenario (e.g. approved_full_features, declined_face_mismatch, in_review_aml_hit, business_session_approved, transaction_created, activity_created, user_status_updated) and send a fully-formed webhook to your destination URL.
  • Every scenario is signed with the same three signature headers and canonical encoding as production traffic, including unicode test data (approved_kyc_with_unicode) so you can confirm X-Signature-V2 works through your middleware. Test deliveries carry sample data and are marked with an extra X-Didit-Test-Webhook: true header (and a (Test) suffix on the User-Agent) so you can keep them out of production state.
  • Replay any historic delivery from a destination’s Deliveries tab if you need to debug a regression.
For end-to-end smoke tests, run a real verification through a test workflow in your own application — the resulting webhooks use the same envelope, headers, and retry policy as the Try Webhook scenarios.

Examples

Approved KYC session (status.updated)

Declined session with warnings (status.updated)

Business session approved (status.updated, KYB)

Transaction created (transaction.created)

Travel Rule exchange status changed (travel_rule.status.updated)

travel_rule_status carries one of the 13 exchange statuses documented in Travel Rule. rail is INTERNAL, TRP, or EMAIL (empty string if no rail could be resolved), and direction is INBOUND or OUTBOUND. An after-deposit sunrise registration that mints a new inbound transaction also emits a transaction.created event for that transaction, before the travel_rule.status.updated transitions.

User entity status changed (user.status.updated)

Resubmitted session

Legacy: X-Signature with raw body

X-Signature is still sent on every delivery; it is HMAC-SHA256 over the exact bytes Didit transmits (json.dumps(..., sort_keys=True, separators=(",", ":"))). Use it only when you can read the raw body before any middleware modifies it.

Legacy: V2 webhook format

The fields below only apply when a destination is pinned to webhook_version: "v2". An even older "v1" pin also exists (a third, singular decision shape) for destinations created before V2 — contact support before relying on it. New integrations should use V3 (the default), which is documented above and uses plural arrays.
If a downstream consumer still needs V2 shapes, create a separate destination pinned to webhook_version: "v2" and keep your new consumers on V3.
  • Webhook destinations API — list, create, update, delete destinations and inspect delivery logs.
  • Create webhook destination — request/response schema including the once-returned secret_shared_key.
  • Data models — canonical schema for every per-feature object embedded in decision.
  • Decision schema — full field list for the decision object embedded in session webhooks.
  • Verification statuses — exact status values and transitions referenced above.
  • Rate limiting — limits that apply when polling GET /v3/session/{id}/decision/ as a fallback.