# Webhooks & events

> Signed webhooks for order, search and dispute events, how to verify the signature, the retry schedule, and the events list to poll when a delivery is missed.

- **HTML:** https://offendersearch.app/docs/fcra/webhooks
- **Base URL:** https://api.offendersearch.app
- **Authentication:** `X-API-Key` request header, on an account with FCRA Partner Access
- **FCRA API reference as markdown:** https://offendersearch.app/docs/fcra.md

## Configuration

Set your webhook URL with `PUT /v1/fcra/webhook-endpoint` and `{ "url": "https://…" }`. The response contains the signing `secret` — shown **only** when the endpoint is created or when you send `"rotateSecret": true`, so store it then. `GET` shows whether an endpoint is configured; `DELETE` removes it. A `callbackUrl` on an order sends that order’s events to a different URL, signed with the same secret.

## Events

| Event | When |
| --- | --- |
| `order.created` | An order was accepted. |
| `search.completed` | One search in an order finished — read it. |
| `order.completed` | Every search in the order finished. |
| `order.cancelled` | The order was cancelled. |
| `dispute.created` | A dispute was opened on one of your orders — by you or through the consumer portal. |
| `dispute.updated` | A dispute’s status changed. |
| `record.corrected` | A record you received was modified or removed by a dispute. |

The payload carries identifiers and statuses only — **never personal data**. Fetch the order, search or dispute with your key to read the detail.

Webhook payload

```json
{
  "id": "evt_5f20c1",
  "type": "search.completed",
  "createdAt": "2026-10-02T14:04:12Z",
  "data": { "orderId": "fo_7c1e9b2a4d3f", "searchId": "fs_b52e", "status": "complete" }
}
```

## Verifying the signature

Each delivery carries `X-Offendersearch-Signature: t=<unix time>,v1=<hex HMAC-SHA256>`, computed over `t.` followed by the raw request body with your webhook secret, plus `X-Offendersearch-Event` (the type) and `X-Offendersearch-Event-Id`. Reject a delivery whose signature does not match or whose timestamp is more than five minutes old.

Verify a delivery (Node)

```javascript
import crypto from 'node:crypto';

// header: X-Offendersearch-Signature: t=1791000252,v1=5d41402a…
export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
```

## Retries, resends and polling

Respond with any 2xx. A failed delivery is retried with backoff — about every minute for the first ten minutes, then hourly for 24 hours. `POST /v1/fcra/events/{id}/resend` delivers an event again, and `GET /v1/fcra/events?after=<eventId>&limit=` returns `{ events, nextAfter }` — every event for your account in order, each with its delivery status (`pending`, `delivered`, `failed`, or `no_endpoint` when none is configured) — so a missed delivery is never lost. Events can arrive out of order; read the object’s current status rather than inferring it from the event sequence.

---

## Related

- Previous: [The consumer portal](https://offendersearch.app/docs/fcra/consumer-portal.md)
- Next: [Sandbox](https://offendersearch.app/docs/fcra/sandbox.md)
- Index: [FCRA API reference](https://offendersearch.app/docs/fcra.md)
