Skip to main content

Webhooks & Callbacks

Webhooks let the platform push events to the Company's backend instead of it polling. They carry the result of a verification, events a workflow sends on its way, and changes operators make to stored records. Webhooks are optional — a Company with no endpoints configured can read results from the Customer API instead.

What gets delivered​

Event typeFires whenData tier
workflow.completedA verification ends with an accept or reject decision. Carries the result and the verified customer data.All tiers
workflow.eventA workflow reaches an emit_event step. Carries a Company-defined event name and payload.All tiers
record.actionA back-office operator changes a stored record — accept, reject, reset to review, blocklist, anonymize, merge, resolve as unique, edit fields.Stored tier only

A verification that ends in review sends no workflow.completed. When an operator later decides it, the decision arrives as a record.action (Accept or Reject).

Changes made through the Customer API do not produce record.action events — your backend made them, so it already knows.

Configuration​

Endpoints are registered by an Administrator in Integration Settings → Webhooks, per environment.

  • One or more endpoints. Each is an https:// URL on a public host; private, loopback and link-local addresses are refused.
  • A signing secret per endpoint. It is shown once, when you add the endpoint or rotate its secret. Copy it then; it cannot be displayed again. Rotating replaces it immediately, so update your receiver first.
  • Active or paused. A paused endpoint receives nothing until it is enabled again.
  • Every endpoint receives every event type. Branch on eventType in your receiver and acknowledge the types you do not use.
  • Include selfie in webhook — whether workflow.completed carries the selfie crop. On by default.

A verification sends webhooks only if the tenant had at least one active endpoint when its session was created.

Moving from the single callback URL

Webhooks replace the earlier single Callback URL and its VERIFICATION_SUCCEEDED callback. An existing callback URL was carried over as an endpoint, with a signing secret generated by the platform that has never been shown. To keep receiving results:

  1. In Integration Settings → Webhooks, Rotate secret on the carried-over endpoint and store the new secret on your receiver.
  2. Verify the X-IDV-Signature header.
  3. Read the result from the workflow.completed envelope instead of the old event / data body. The customer fields keep their names; they now sit under payload.customer.

Envelope​

Every delivery is an HTTP POST with a JSON body in a common envelope:

{
"eventId": "0195a1b2-c3d4-7890-abcd-ef1234567891",
"eventType": "record.action",
"tenantId": "550e8400-e29b-41d4-a716-446655440000",
"occurredAt": "2026-01-01T12:00:00Z",
"payload": { }
}
FieldDescription
eventIdUnique per event. Retries and replays of the same event keep it — use it to de-duplicate.
eventTypeworkflow.completed, workflow.event or record.action.
tenantIdYour tenant.
occurredAtWhen the event happened (ISO 8601, UTC).
payloadEvent-specific, see below.

Fields are added over time, so ignore unknown properties rather than rejecting the payload. The full schemas are in the Customer API reference under Webhooks.

workflow.completed​

The result of a verification, with the verified customer data. This is how a Company that does not read records back over the Customer API — including every tenant on the session-based tier — receives the data.

{
"eventId": "…",
"eventType": "workflow.completed",
"tenantId": "…",
"occurredAt": "2026-01-01T12:00:00Z",
"payload": {
"workflowId": "onboarding_v1",
"workflowRunId": "…",
"sessionToken": "…",
"result": "VERIFIED",
"timestamp": 1767268800000,
"customer_id": "…",
"digitalIdentityId": "…",
"customer": {
"fullName": "Jane Doe",
"firstName": "Jane",
"lastName": "Doe",
"dateOfBirth": "1990-05-17",
"documentNumber": "…",
"address": { "fullAddress": "…" },
"selfie": "…"
},
"trustFactors": [
{ "name": "age_verification", "state": "accepted", "mandatory": true }
]
}
}
FieldDescription
workflowIdName of the workflow that ran.
workflowRunIdIdentifier of the run.
sessionTokenThe session you created, for correlation only. It expires with the session and cannot be used to fetch data.
resultVERIFIED or REJECTED.
timestampCompletion time, Unix epoch milliseconds.
customer_idExternal identifier of the Customer, on the stored tier when the run produced or updated one.
digitalIdentityIdIdentifier of the stored Digital Identity. null when no record was stored, together with digitalIdentityIdUnavailableReason.
customerVerified data read from the document — the fields are listed under Website Redirect → Customer data. Empty fields are omitted.
trustFactorsEach trust factor of the run: name, state (accepted, review, rejected, skipped), mandatory, and reason: "missing_input" when the state comes from missing input. Absent when the run evaluated none.

Only the first delivery attempt carries the selfie. A retry or a replay sends customer.selfie as null with customer.selfieUnavailableReason.

workflow.event​

Sent by an emit_event step each time a run reaches it. The event name and data are whatever the workflow defines.

{
"eventType": "workflow.event",
"payload": {
"workflowId": "onboarding_v1",
"workflowRunId": "…",
"eventName": "onboarding.decided",
"stepKey": "emit_event",
"stepOrder": 8,
"outcomeName": "accept_unique",
"data": { "outcome": "accept_unique", "trust": "accept", "channel": "web" }
}
}
FieldDescription
eventNameThe step's event parameter.
stepKey · stepOrderWhich step sent it.
outcomeNameThe matched outcome. Present only when a decision has already matched one.
dataThe step's resolved payload. Absent when the step has none.

A workflow event is not transactional with the outcome: it can arrive before the records the outcome creates exist.

record.action​

One event per operator action in the back office — per button, or per Save for field edits. Payloads are diff-only: they say what changed, not the whole record. Fetch the record from the Customer API when you need it.

{
"eventType": "record.action",
"payload": {
"recordType": "DigitalIdentity",
"recordId": "…",
"action": "Accept",
"actor": { "operatorId": "…", "operatorEmail": "operator@company.example", "role": "Operator" },
"transition": { "trustFactorState": { "from": "review", "to": "accepted" } }
}
}
FieldDescription
recordTypeDigitalIdentity or Customer.
recordIdExternal identifier of the record, as used by the Customer API.
actionOne value of the closed list below.
actorWho acted: operatorId, operatorEmail, role (Operator, Supervisor, Administrator).
transitionWhat changed — see below.
commentThe operator's reason, on ResetToReview.
Recordactiontransition
Digital IdentityAccept · RejecttrustFactorState from the previous decision to accepted / rejected
Digital IdentityResetToReviewtrustFactorState to review; also deduplicationState to unresolved when it had been resolved
Digital IdentityResolveAsUniquededuplicationState to unique
Digital IdentityMergeWithCustomer · MergeWithBlocklistdeduplicationState to merged
Digital IdentityPutOnBlocklisttrustFactorState to rejected
Digital IdentityAnonymizetrustFactorState to anonymized
CustomerEditFieldfieldChanges[] — one entry per changed field (name, given_name, surname, place_of_birth, date_of_birth) with from and to
CustomerPutOnBlocklistfieldChanges[] with status from active to blocked
CustomerAnonymizenone

trustFactorState values are the Digital Identity decisions in lower case (pending, incomplete, review, accepted, rejected, anonymized); deduplicationState values are unresolved, unique and merged.

The event is queued in the same transaction as the change, so every committed action produces exactly one event.

Verifying authenticity​

Each delivery carries an X-IDV-Signature header of the form sha256=<hex>: an HMAC-SHA256 of the raw request body, keyed with the endpoint's signing secret. Recompute it and compare before trusting the payload.

Use the secret exactly as shown in the dashboard, as a text string — do not hex-decode it. Compute the digest over the bytes you received; re-serializing parsed JSON changes the bytes and the signature will never match.

const { createHmac, timingSafeEqual } = require('node:crypto');

// rawBody: the request body exactly as received (string or Buffer).
function isAuthentic(rawBody, signatureHeader, secret) {
if (!signatureHeader) return false;
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(signatureHeader);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}
import hashlib
import hmac

def is_authentic(raw_body: bytes, signature_header: str | None, secret: str) -> bool:
if not signature_header:
return False
expected = "sha256=" + hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature_header, expected)

Reject a request whose signature does not match. A 4xx reply is final: the platform does not retry it and lists it as a failed delivery.

Delivery guarantees​

PropertyBehavior
Off the critical pathWebhooks are sent in the background after the decision. A slow or unreachable endpoint never delays the verification result returned to the user.
AcknowledgementAny 2xx response acknowledges the event. Each attempt waits up to 30 seconds for a response.
At-least-onceFailed attempts are retried with exponential backoff — first after about 1 minute, doubling each time, at most 4 hours apart — for up to 24 hours.
Retryable5xx, 408, 429 and connection failures are retried. Every other 4xx is treated as a permanent refusal and is not retried.
IdempotencyDe-duplicate on eventId — the same event can arrive more than once, from a retry or a replay.
OrderingNot guaranteed, even for one record. Use occurredAt if order matters.
Failed deliveriesA delivery that is refused with a non-retryable 4xx, or that is still failing after 24 hours, is listed as a failed delivery and can be replayed — see below.

Failed deliveries and replay​

Integration Settings → Webhooks → Failed webhook deliveries lists every event that was not delivered, with the endpoint URL, event type, eventId, the last HTTP status, and when it failed. The Webhooks tab shows a banner while any are waiting.

Replay sends the event again, with its original eventId, and the normal retry schedule applies. A replay is offered to every active endpoint that accepts the event, so an endpoint that already received it may see it again — de-duplicating on eventId covers this. A replayed event differs from the original only where its content has expired:

  • workflow.completed has no sessionToken; sessionTokenExpired is true instead. The selfie is null.
  • If the person was anonymized after the delivery failed, the personal data is removed from the stored event and payload.personalDataRemoved is true. Only the identifiers of the run or action remain.

See also​