Offendersearch
FCRA API Reference

Webhooks & events

Signed, retried, identifiers-only events — and the list to poll if one is missed.

Base URL https://api.offendersearch.app

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

EventWhen
order.createdAn order was accepted.
search.completedOne search in an order finished — read it.
order.completedEvery search in the order finished.
order.cancelledThe order was cancelled.
dispute.createdA dispute was opened on one of your orders — by you or through the consumer portal.
dispute.updatedA dispute’s status changed.
record.correctedA 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
{
  "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)
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.