> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fastcac.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Get told when a registration changes, instead of polling.

FastCAC sends an HTTPS `POST` to your endpoint whenever something happens: a registration is
submitted, queried, approved, a document is rejected. Each test-mode and live-mode event goes to
the endpoints of that mode, so test traffic never reaches your production endpoint.

## Add an endpoint

In the dashboard under **Webhooks**, or with the API:

```bash theme={null}
curl https://api.fastcac.com/v1/webhooks/endpoints \
  -H "Authorization: Bearer $FASTCAC_KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/fastcac/webhooks", "description": "Production handler"}'
```

* The URL must be public HTTPS. Private and local addresses are refused.
* Up to 10 endpoints per mode. Limit one to some events with `event_types`; leave it out (`null`)
  to receive everything.
* Each endpoint has its own signing secret (`whsec_…`):
  `GET /v1/webhooks/endpoints/{id}/secret`. Rotate it with `POST …/secret/rotate`.
* Send a sample event to check your handler: `POST /v1/webhooks/endpoints/{id}/test` with
  `{"event_type": "registration.approved"}`.

## What you receive

Every event has the same envelope. `data` is thin: ids and statuses, never personal data. Fetch the
object with your key when you need the details.

```json registration.approved theme={null}
{
  "id": "evt_0000000000000000000000",
  "type": "registration.approved",
  "api_version": "2026-11-01",
  "created_at": "2026-11-03T10:14:22Z",
  "data": {
    "object": "registration",
    "id": "reg_4Pd0kXq2ZbN1sW8yT3mVhL",
    "business_id": "biz_7Gk2Lm9QwRt5Yp3Xn8Dv1A",
    "external_id": "order-1182",
    "status": "approved",
    "previous_status": "submitted"
  }
}
```

| Field | Meaning |
| - | - |
| `id` | The event (`evt_…`). Unique; use it to ignore duplicates. |
| `type` | One of the [event types](https://docs.fastcac.com/webhooks/events.md). |
| `api_version` | The API version the payload follows. |
| `created_at` | When it happened (UTC). |
| `data` | The object that changed, with its new `status` and `previous_status`. |

Requests carry three headers: `webhook-id` (the same as `id`), `webhook-timestamp` and
`webhook-signature`. [Verify the signature](https://docs.fastcac.com/webhooks/verify-signatures.md) before trusting a
request.

## Responding

Return any `2xx` status to acknowledge the event, within 30 seconds. Do the slow work afterwards
(queue it). Anything else, or no answer in time, counts as a failure.

## Retries

A failed delivery is retried after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours
and 10 hours: about 27 hours in all. An endpoint that has failed every delivery for 5 days is
disabled; re-enable it with `PATCH /v1/webhooks/endpoints/{id}` and `{"disabled": false}`.

To replay what an endpoint missed, `POST /v1/webhooks/endpoints/{id}/recover` with a `since` time
resends every failed delivery since then. Resend one event with
`POST /v1/webhooks/events/{id}/resend`.

## Writing a good handler

* **Verify first**, using the raw request body.
* **Expect duplicates.** Retries and resends deliver the same `id` again. Record ids you've handled
  and skip repeats.
* **Don't rely on order.** Events can arrive out of order, especially after retries. Treat an event
  as a nudge: fetch the registration and act on its current `status`.
* **Acknowledge fast** and process in the background.

## Seeing deliveries

`GET /v1/webhooks/events` lists recent events, `GET /v1/webhooks/events/{id}` shows one with every
delivery attempt, and `GET /v1/webhooks/endpoints/{id}/attempts` shows an endpoint's attempts with
the status and body your server returned. Payloads are kept for 30 days.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.