# 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_type, action, object, 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_timeline_file.created` | A timeline file has been generated for a workflow run |
| `workflow_run_evidence_folder.created` | An evidence folder has been created for a workflow run |

### General events

| Event | Fires when |
| --- | --- |
| `watchlist_monitor.matches_updated` | 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:

```json
{
  "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:

```javascript
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:

```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](/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](/docs/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](/docs/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](/docs/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](/webhooks/guides/implement-webhook-idempotency) and [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### 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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle Onfido webhooks reliably in minutes.