Gareth Wilson Gareth Wilson

Guide to Onfido Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationDashboard Developers > Webhooks, or POST /v3.6/webhooks/
LimitUp to 20 webhooks
EnvironmentsLive, sandbox or both, per webhook
Event selectionAll events by default, or an events array
Body{ "payload": { resource_type, action, object, resource } }
Signature headerX-SHA2-Signature
SchemeHMAC-SHA256, hex digest
Signed contentThe raw request body, nothing else
SecretThe webhook's token, returned on registration and shown in the dashboard
Replay windowNone. No timestamp is signed
ResponseAny 20x within 10 seconds
Retries5 resends: 30 seconds, 2 minutes, 15 minutes, 2 hours and 10 hours after the first attempt
Circuit breaker5 failures in a row disable the webhook for one minute
DeliveryAt least once, in no guaranteed order
TransportHTTPS only, TLS 1.2 or 1.3
Source IPsThree each for Europe, the United States and Canada
Manual resendPOST /v3.6/webhooks/resend, for events in the last 30 days
Optional extra authAn 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

EventFires when
workflow_run.completedA workflow run has been completed
workflow_task.startedA new task of a workflow run has started
workflow_task.completedA workflow task has been completed
workflow_timeline_file.createdA timeline file has been generated for a workflow run
workflow_run_evidence_folder.createdAn evidence folder has been created for a workflow run

General events

EventFires when
watchlist_monitor.matches_updatedAn ongoing watchlist monitor has new matches
audit_log.createdA dashboard audit log is created (audit logs must be enabled)

Checks and reports events

EventFires when
check.startedA check has started
check.reopenedA check was reopened and the applicant must resubmit information
check.withdrawnA check has been withdrawn
check.completedA check has been completed
check.form_completedAn applicant submitted their information using the check form
report.withdrawn, report.resumed, report.cancelledA report was withdrawn, or a paused report resumed or was cancelled
report.awaiting_approvalA report moved to Awaiting Approval
report.completedA 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_monitor.matches_updated 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

  1. In the Onfido dashboard, click Developers, select the Webhooks tab and press Create webhook.
  2. Enter your HTTPS URL and a name, choose sandbox, live or both, and pick the events you need.
  3. Copy the webhook's token from the Webhook Management page and store it server-side, for example as ONFIDO_WEBHOOK_TOKEN.
  4. 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:

RegionIP addresses
Europe52.51.171.25, 52.51.228.228, 52.51.234.203
United States34.232.2.222, 52.55.124.58, 34.224.182.19
Canada15.223.105.11, 15.222.176.53, 15.222.71.172

Make Onfido webhooks production-ready. Hookdeck Event Gateway verifies the X-SHA2-Signature header, 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/resend to 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 action together with object.id.
  • Fetch the current state from object.href instead 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.