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

# Webhooks

> Subscribe an HTTPS endpoint to events, verify each delivery with its signing secret, and read the delivery log when something goes wrong.

## What is a webhook endpoint?

A webhook endpoint is an HTTPS URL you register to receive events as they happen. `GET /v1/webhooks/event-types` lists every event type Endstate emits; the examples in this guide use the placeholder `example.updated`. Each delivery is a signed `POST` with a JSON envelope. You can register up to 16 endpoints per organization, each subscribed to its own set of event types.

## Registering an endpoint

Call `POST /v1/webhooks/endpoints` with the URL and the event types to subscribe to. The response includes the endpoint's signing secret. Store it; you need it to verify deliveries. The URL must use `https` and resolve to a public host.

The list is the same for every organization and grows as products add events.

## The delivery

Every delivery carries three headers in the [Standard Webhooks](https://www.standardwebhooks.com) format:

| Header | Meaning |
| - | - |
| `webhook-id` | The event id. It is the same on every retry of the same event, so you can deduplicate on it. |
| `webhook-timestamp` | Unix seconds when the delivery was signed. Reject anything more than five minutes old or in the future. |
| `webhook-signature` | One or more `v1,<base64>` signatures. Each is HMAC-SHA256 over `id.timestamp.body` with a secret. |

The body is the event envelope:

```json theme={null}
{
  "id": "3f1c9a2b-7d4e-4f10-9a8b-2c6d5e4f3a21",
  "type": "example.updated",
  "timestamp": "2026-09-22T12:00:00Z",
  "api_version": "2026-09-01",
  "organization_id": "11111111-1111-4111-8111-111111111111",
  "audience": "brand",
  "resource": {
    "type": "example",
    "id": "6b1e2a3c-4d5e-4f60-8172-839405162738",
    "revision": 1
  },
  "data": {}
}
```

Respond with any `2xx` once you have durably recorded the event. Anything else, or no response within 15 seconds, is a failure and the delivery is retried: 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 10 hours. After the eighth attempt the event is marked failed for that endpoint; the attempt log shows every try.

Delivery is at least once. If your handler processed the event but timed out before responding, the same `webhook-id` arrives again.

## Verifying a delivery

Verify before you parse. Compute HMAC-SHA256 over `${id}.${timestamp}.${rawBody}` with the secret's bytes (the part after `whsec_`, base64-decoded), base64-encode the result, and compare it in constant time against each `v1,` value in `webhook-signature`. Accept the delivery if any matches and the timestamp is within tolerance. Any Standard Webhooks library does this for you.

## Secrets

The secret is returned when you create the endpoint and again from `POST .../rotate-secret`. An idempotent replay of either request returns the endpoint without the secret, so store it when you first receive it. An admin API key can read it at any time with `GET /v1/webhooks/endpoints/{id}/secret`; every read and every rotation is recorded.

After a rotation, deliveries carry a signature from the new secret and one from the previous secret for 24 hours. Switch your verifier to the new secret at any point in that window without missing events.

## When an endpoint is disabled

Every failed delivery to an otherwise healthy endpoint sets `fail_since` on the endpoint to the time the trouble started; a successful delivery clears it. Watch it: the next failed delivery after `fail_since` is five days old moves the endpoint to `disabled`, and a `410 Gone` response disables it immediately. Deliveries then stop and `disabled_reason` says why.

Re-enable with `PATCH /v1/webhooks/endpoints/{id}` and `{ "status": "enabled" }`. That clears `fail_since` and starts the five-day clock over. Re-enabling does not resend what was missed; recover it, below.

You can also pause an endpoint yourself, for example while its receiver is being redeployed: the same `PATCH` with `{ "status": "disabled" }` stops deliveries and sets `disabled_reason` to `disabled by request`. Re-enable and recover the same way.

## Testing an endpoint

`POST /v1/webhooks/endpoints/{id}/test` with `{ "event_type": "..." }` sends the catalog's example payload for that type to this endpoint only. The event has `audience: "test"` and is otherwise a normal delivery: signed with the endpoint's secret, retried on the usual schedule, and counted toward endpoint health, so a receiver that fails a test shows up in `fail_since` and one that succeeds clears it. The response carries the event and its first attempt; follow the rest through the event's attempts.

## Recovering missed events

`POST /v1/webhooks/endpoints/{id}/recover` with `{ "since": "<time>" }` queues a delivery for every event the endpoint subscribes to that was created since that time and never reached it, for example while it was disabled, and the next attempt for every event whose latest attempt to it failed. Events that got through, or still have an attempt pending, are left alone, so recovering the same window twice queues nothing the second time. The response says how many were queued; deliveries start within about a minute and follow the normal retry schedule. `since` must be within the last 30 days.

## Redelivering one event

`POST /v1/webhooks/events/{id}/redeliver` with `{ "endpoint_id": "..." }` sends that event to that endpoint now. If a retry is already scheduled for later, that retry goes out at once instead of waiting; otherwise one more attempt is made, for example after the retry schedule was exhausted. It is refused only while an attempt is being sent at that moment, and, for an event that never reached the endpoint, unless the endpoint subscribes to its type.

## Reading the delivery log

`GET /v1/webhooks/events` lists events, `GET /v1/webhooks/events/{id}` returns one event with the `data` payload it was delivered with, and `GET /v1/webhooks/events/{id}/attempts` shows every attempt for one event: status, response code, duration, and the endpoint it went to. Attempts to endpoints you have since deleted are still listed, marked `deleted: true`.

## Deleting an endpoint

`DELETE /v1/webhooks/endpoints/{id}` stops deliveries and removes the endpoint from lists. Its delivery history remains readable through webhook events.


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