Guide to Onfido Webhooks: Features and Best Practices
Onfido, now part of Entrust and branded Entrust Identity Verification, verifies people's identities: documents, selfies, watchlist screening and the workflows that combine them. Verification takes time. An applicant uploads a document, checks run, a reviewer may step in, and only then is there a result. Webhooks are how your application finds out that a workflow run has finished, without polling the API.
This guide covers how Onfido webhooks are registered and signed, the events for Workflow Studio and for the older Checks and Reports API, how retries and the circuit breaker work, and the best practices for running an Onfido receiver in production. The documentation now says Entrust in most places, but the API, the dashboard and the webhook headers still use the Onfido name.
What are Onfido webhooks?
An Onfido webhook is a registered HTTPS URL plus a list of events. When one of those events happens in the environment the webhook listens to, Onfido sends a POST with a JSON body describing it. You can register up to 20 webhooks, each receiving events from live, sandbox or both.
Every body has a single payload object. Inside it, action is the event name, such as workflow_run.completed, and object identifies what changed, with an id, a status and an href to fetch it from the API. Workflow Studio events also include resource, the full object.
Each webhook has its own secret token. Onfido signs every request with it and puts a hex HMAC-SHA256 of the raw body in the X-SHA2-Signature header.
Onfido webhook features
| Feature | Details |
|---|---|
| Configuration | Dashboard Developers > Webhooks, or POST /v3.6/webhooks/ |
| Limit | Up to 20 webhooks |
| Environments | Live, sandbox or both, per webhook |
| Event selection | All events by default, or an events array |
| Body | { "payload": { resource_ |
| Signature header | X-SHA2-Signature |
| Scheme | HMAC-SHA256, hex digest |
| Signed content | The raw request body, nothing else |
| Secret | The webhook's token, returned on registration and shown in the dashboard |
| Replay window | None. No timestamp is signed |
| Response | Any 20x within 10 seconds |
| Retries | 5 resends: 30 seconds, 2 minutes, 15 minutes, 2 hours and 10 hours after the first attempt |
| Circuit breaker | 5 failures in a row disable the webhook for one minute |
| Delivery | At least once, in no guaranteed order |
| Transport | HTTPS only, TLS 1.2 or 1.3 |
| Source IPs | Three each for Europe, the United States and Canada |
| Manual resend | POST /v3.6/webhooks/resend, for events in the last 30 days |
| Optional extra auth | An OAuth client-credentials Bearer token per webhook |
Onfido webhook events
Which events matter depends on how you integrate. New integrations use Workflow Studio, and the checks and reports events are only for customers who haven't migrated from the older Checks and Reports API.
Workflow Studio events
| Event | Fires when |
|---|---|
workflow_run.completed | A workflow run has been completed |
workflow_task.started | A new task of a workflow run has started |
workflow_task.completed | A workflow task has been completed |
workflow_ | A timeline file has been generated for a workflow run |
workflow_ | An evidence folder has been created for a workflow run |
General events
| Event | Fires when |
|---|---|
watchlist_ | An ongoing watchlist monitor has new matches |
audit_log.created | A dashboard audit log is created (audit logs must be enabled) |
Checks and reports events
| Event | Fires when |
|---|---|
check.started | A check has started |
check.reopened | A check was reopened and the applicant must resubmit information |
check.withdrawn | A check has been withdrawn |
check.completed | A check has been completed |
check.form_completed | An applicant submitted their information using the check form |
report.withdrawn, report.resumed, report.cancelled | A report was withdrawn, or a paused report resumed or was cancelled |
report.awaiting_approval | A report moved to Awaiting Approval |
report.completed | A report has been completed and results are available |
Here is a workflow_run.completed request from Onfido's documentation, trimmed:
{
"payload": {
"resource_type": "workflow_run",
"action": "workflow_run.completed",
"object": {
"id": "36a56899-d617-4988-85bf-b76640861bf5",
"status": "abandoned",
"completed_at_iso8601": "2022-12-19T16:54:44Z",
"href": "https://api.onfido.com/v3.6/workflow_runs/36a56899-d617-4988-85bf-b76640861bf5"
},
"resource": {
"id": "36a56899-d617-4988-85bf-b76640861bf5",
"applicant_id": "9bb725cb-c9a4-4b46-bfc7-d62965a32d9a",
"workflow_id": "f96b8c57-8084-4ca7-bc4c-f737e1cb1718",
"status": "abandoned",
"dashboard_url": "https://dashboard.onfido.com/results/36a56899-d617-4988-85bf-b76640861bf5",
"created_at": "2022-12-05T16:54:44Z",
"updated_at": "2022-12-19T16:54:44Z",
"output": null
}
}
}
Onfido warns that some payloads carry personal data, including audit_log.created, watchlist_ and the workflow_* events. Timeline files and evidence folders may contain personal information, and their download URLs are pre-signed, so treat them as secrets.
Setting up Onfido webhooks
- In the Onfido dashboard, click Developers, select the Webhooks tab and press Create webhook.
- Enter your HTTPS URL and a name, choose sandbox, live or both, and pick the events you need.
- Copy the webhook's token from the Webhook Management page and store it server-side, for example as
ONFIDO_WEBHOOK_TOKEN. - For Workflow Studio, open each workflow version in the Workflow Builder and select the webhooks to apply under Select webhooks.
You can do the same with the API. POST /v3.6/webhooks/ takes url, name, enabled, environments, events and payload_version, and returns a webhook object whose token field is the signing secret. You can't use a sandbox API token to create or manage webhooks for the live environment. With a live token and no environments, the webhook receives events from both.
Each webhook can also be set to fetch an OAuth access token from your own authorisation server, using the client credentials grant, and send it as a Bearer token in the Authorization header. Onfido caches the token for 5 minutes per webhook. This adds to the signature rather than replacing it.
Securing Onfido webhooks
Compute an HMAC-SHA256 of the raw request body with the webhook's token, hex-encode it, and compare it with X-SHA2-Signature in constant time. Onfido's client libraries for Java, Node, PHP, Python and Ruby include a verifier, which needs the same raw body. If you write your own, it looks like this:
const crypto = require('crypto');
const express = require('express');
function verifyOnfidoSignature(rawBody, signatureHeader, token) {
if (!signatureHeader || !token) return false;
const expected = crypto.createHmac('sha256', token).update(rawBody).digest('hex');
const received = Buffer.from(signatureHeader.trim().toLowerCase(), 'utf8');
const computed = Buffer.from(expected, 'utf8');
return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}
const app = express();
app.post('/webhooks/onfido', express.raw({ type: '*/*' }), (req, res) => {
const token = process.env.ONFIDO_WEBHOOK_TOKEN;
if (!token) return res.sendStatus(500); // fail closed
if (!verifyOnfidoSignature(req.body, req.get('x-sha2-signature'), token)) {
return res.sendStatus(401);
}
const { payload } = JSON.parse(req.body.toString('utf8'));
const { action, object } = payload; // e.g. workflow_run.completed
// Store it, then process asynchronously. Onfido waits 10 seconds.
res.sendStatus(200);
});
The same check in Python:
import hashlib
import hmac
def verify_onfido_signature(raw_body: bytes, signature_header, token: str) -> bool:
if not signature_header or not token:
return False
expected = hmac.new(token.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
received = signature_header.strip().lower().encode("utf-8")
return hmac.compare_digest(received, expected.encode("utf-8"))
# In FastAPI: raw_body = await request.body()
# verify_onfido_signature(raw_body, request.headers.get("x-sha2-signature"),
# os.environ["ONFIDO_WEBHOOK_TOKEN"])
To check your verifier, use the token onfido-test-webhook-token and this exact body:
{"payload":{"resource_type":"workflow_run","action":"workflow_run.completed","object":{"id":"36a56899-d617-4988-85bf-b76640861bf5","status":"approved"}}}
The signature should be:
c0945ce99b8ec2c25f73ad2d1179781df76f6ba8ee71bce681132b1ae0fb8318
Each webhook has its own token, so if you register more than one, make sure each endpoint checks against the right one. Onfido also publishes the IP addresses webhooks come from, which you can allowlist as an extra layer:
| Region | IP addresses |
|---|---|
| Europe | 52.51.171.25, 52.51.228.228, 52.51.234.203 |
| United States | 34.232.2.222, 52.55.124.58, 34.224.182.19 |
| Canada | 15.223.105.11, 15.222.176.53, 15.222.71.172 |
Make Onfido webhooks production-ready. Hookdeck Event Gateway verifies the
X-SHA2-Signatureheader, answers inside Onfido's 10-second window, and keeps your webhook from being switched off by Onfido's circuit breaker when your service has problems.
Onfido webhook limitations and pain points
A short retry schedule and a circuit breaker
The Problem: An outage of more than half a day loses verification results, and a brief incident can make deliveries stop altogether for a while.
Why It Happens: Onfido resends a failed notification five times, the last one 10 hours after the first attempt. Separately, if any five requests to the same webhook fail in a row, Onfido disables that webhook for one minute, which holds up events for every applicant during that minute.
Workarounds:
- Monitor failures in the dashboard's webhook logs.
- Use
POST /v3.6/webhooks/resendto re-send events for up to 20 resources at a time, for events in the past 30 days. - Reconcile by fetching workflow runs from the API.
How Hookdeck Can Help: Hookdeck answers Onfido at its edge, so your service's failures don't count against Onfido's retries or trip the circuit breaker. Hookdeck stores each event and retries delivery to your service on a schedule you set.
A 10-second response window
The Problem: Deliveries your service handled are still treated as failures and resent.
Why It Happens: Onfido expects a 20x within 10 seconds. Fetching the workflow run, downloading documents or writing to several systems in the handler can take longer.
Workarounds:
- Verify and store the event, respond, and do the rest in a background job.
- Make processing idempotent, because slow responses produce resends.
How Hookdeck Can Help: Hookdeck acknowledges each delivery immediately and then delivers to your service with a timeout you choose.
Duplicates and out-of-order events
The Problem: The same event arrives twice, or workflow_task.completed arrives after workflow_run.completed.
Why It Happens: Onfido guarantees at-least-once delivery and doesn't guarantee order. Retries and resends make both more likely.
Workarounds:
- Deduplicate on
actiontogether withobject.id. - Fetch the current state from
object.hrefinstead of relying on the order events arrive in.
How Hookdeck Can Help: Hookdeck deduplication can drop repeated payloads before they reach your service, and its event log shows when each one arrived.
Personal data in payloads
The Problem: Webhook payloads, logs and replay tools end up holding identity data.
Why It Happens: Workflow events include the full workflow run, watchlist and audit log events include personal details, and timeline files and evidence folders come with pre-signed download URLs.
Workarounds:
- Subscribe only to the events you need.
- Avoid logging whole bodies, and apply the same retention rules to webhook storage as to the rest of your identity data.
How Hookdeck Can Help: Transformations can remove fields you don't need before an event reaches your service. Check that Hookdeck's retention settings fit your data policy before you route personal data through it.
Best practices
Verify against the raw body
Mount express.raw() on the webhook route and parse only after verification. In FastAPI, read await request.body(). Onfido's own instructions say the body must be raw, not decoded from JSON.
Fetch the resource before acting on it
object carries the id, status and href of what changed. For decisions such as approving an applicant, fetch the latest state from the API, which also guards against out-of-order events.
Keep sandbox and live apart
Register separate webhooks for sandbox and live, each with its own token and endpoint, so test applicants never reach production systems.
Deduplicate and process asynchronously
Record action and object.id before processing, return 20x, and process afterwards. See our guide to webhook idempotency and why to process webhooks asynchronously.
Pin the payload version
Set payload_version when you register a webhook, so changes to the payload format happen when you choose to upgrade.
Conclusion
Onfido signs each webhook with a hex HMAC-SHA256 of the raw body in X-SHA2-Signature, keyed with that webhook's token. The work in production is mostly about delivery: a 10-second window, five resends over 10 hours, a circuit breaker that pauses the whole webhook, at-least-once delivery without ordering, and payloads full of personal data.
Hookdeck Event Gateway sits in front of your receiver, verifies every signature, answers within Onfido's window, and holds each event until your service has processed it.
Get started with Hookdeck for free and handle Onfido webhooks reliably in minutes.