# Webhooks API reference

> The webhook operations of the Fax for Agents API. Register a destination, filter its events, rotate the secret, send a test event and replay a delivery.

Page: https://faxforagents.com/docs/api/reference/webhooks/
Last changed: 2026-11-13

Updated Nov 13, 2026

8 operations of the Fax for Agents API, from the OpenAPI document version 1.20.0, snapshot of 2026-11-13. The base URL is `https://api.faxforagents.com/v1`. This is one group of the [full reference](https://faxforagents.com/docs/api/reference/).

- The secret of a webhook is shown once, in the answer that created or rotated it.
- After a secret rotation the old secret signs too for 24 hours.
- A replay sends the event again with the same event id.

## Operations

- `GET /v1/webhook-deliveries` Delivery attempts (no payload secrets)
- `POST /v1/webhook-deliveries/{delivery_id}/replay` Replay the event with the same event id
- `GET /v1/webhooks` List webhooks
- `POST /v1/webhooks` Register a destination (secret shown once)
- `DELETE /v1/webhooks/{webhook_id}` Remove a destination
- `PATCH /v1/webhooks/{webhook_id}` Change url, filters or pause
- `POST /v1/webhooks/{webhook_id}/rotate-secret` New secret; the old one signs too for 24 hours
- `POST /v1/webhooks/{webhook_id}/test` Send a signed webhook.test event to this destination

## What is a webhook destination?

A URL of yours, an event filter and a secret. `POST /v1/webhooks` registers one. The body needs the `url` and can carry a `description` and `event_types`. An empty or missing list means every event type.

The answer carries the `secret` once. Every delivery to this URL is signed with it. The [webhooks guide](https://faxforagents.com/docs/webhooks/) has the three headers and code that verifies the signature in Python and TypeScript.

## How do I change or pause a destination?

`PATCH /v1/webhooks/{webhook_id}` changes the URL, the description and the filter. It also takes `status`, which pauses a destination and resumes it. A paused destination gets no deliveries. `DELETE /v1/webhooks/{webhook_id}` removes it. `GET /v1/webhooks` lists the destinations of the environment, without their secrets.

## How is a secret rotated?

`POST /v1/webhooks/{webhook_id}/rotate-secret` returns a new secret. The summary states the overlap: the old one signs too for 24 hours. During that time each delivery carries two signatures, and a receiver that accepts either keeps working. `previous_secret_expires_at` on the destination says when the overlap ends.

So a rotation needs no window of downtime. Rotate, deploy the new secret to the receiver, and the old one stops signing by itself.

## How do I test a receiver?

`POST /v1/webhooks/{webhook_id}/test` sends a signed `webhook.test` event to the destination and answers 202 with the event it sent. The receiver sees a real delivery with a real signature, and no fax is involved. Use it right after registering, and again after each rotation.

## Where do I see what was delivered?

`GET /v1/webhook-deliveries` lists the delivery attempts. Each row has the event id and type, the attempt number, the status the receiver answered and the duration. It also has an error when there was one, and when the next attempt is due. The summary adds "no payload secrets". The list filters by `webhook_id` and by `event_id`, and pages with `cursor` and `limit`.

A receiver that answers outside the 2xx range gets the event again on a fixed schedule. The guide has the schedule with its date.

## What does a replay do?

`POST /v1/webhook-deliveries/{delivery_id}/replay` sends the event of that delivery again and answers 202. The event id is the same as the first time. A handler that deduplicates on the id treats a replay of something it already handled as a no-op, and handles one it missed.

For a longer gap, the log in the [events group](https://faxforagents.com/docs/api/reference/events/) returns every event in order, delivered or not.

## Which scope do webhook calls need?

One: `webhook:admin`, for every operation in the group, the lists included. The default API key does not carry it. A call with it answers 403 `scope_missing`. Choose the scope when you create the key that registers webhooks.

An agent key is never issued `webhook:admin`. Where events go is decided by a person or by a key of the organization, not by the agent whose faxes they describe.

## Do test and live events mix?

No. A destination belongs to the environment of the credential that registered it. A test key's destination gets the events of sandbox faxes, and a live key's destination gets the events of real ones.

## Each operation in detail

### GET `/v1/webhook-deliveries`

Delivery attempts (no payload secrets)

Credential: an API key or a signed-in session. Scope: `webhook:admin`.

#### Parameters

GET /v1/webhook-deliveries parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `webhook_id` | query | string or null | no |  |
| `event_id` | query | string or null | no |  |
| `cursor` | query | string or null | no |  |
| `limit` | query | integer | no | Default `50` . |

#### Responses

GET /v1/webhook-deliveries responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/webhook-deliveries response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | array of object Delivery | yes |  |
| `next_cursor` | string or null | no |  |

#### Example

```
`curl https://api.faxforagents.com/v1/webhook-deliveries \ -H "Authorization: Bearer ffa_test_..."`
```

### POST `/v1/webhook-deliveries/{delivery_id}/replay`

Replay the event with the same event id

Credential: an API key or a signed-in session. Scope: `webhook:admin`.

#### Parameters

POST /v1/webhook-deliveries/{delivery_id}/replay parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `delivery_id` | path | string | yes |  |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Responses

POST /v1/webhook-deliveries/{delivery_id}/replay responses

| Status | Meaning |
| --- | --- |
| `202` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

POST /v1/webhook-deliveries/{delivery_id}/replay response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `attempt` | integer | yes |  |
| `created_at` | string | yes |  |
| `delivered_at` | string or null | yes |  |
| `duration_ms` | integer or null | yes |  |
| `error` | string or null | yes |  |
| `event_id` | string | yes |  |
| `event_type` | string | yes |  |
| `id` | string | yes |  |
| `next_attempt_at` | string or null | yes |  |
| `replay_of` | string or null | yes |  |
| `response_status` | integer or null | yes |  |
| `schedule_s` | array of integer | yes |  |
| `status` | string | yes |  |
| `webhook_id` | string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/webhook-deliveries/DELIVERY_ID/replay \ -X POST \ -H "Authorization: Bearer ffa_test_..."`
```

### GET `/v1/webhooks`

List webhooks

Credential: an API key or a signed-in session. Scope: `webhook:admin`.

#### Responses

GET /v1/webhooks responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/webhooks response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | array of object Webhook | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/webhooks \ -H "Authorization: Bearer ffa_test_..."`
```

### POST `/v1/webhooks`

Register a destination (secret shown once)

Credential: an API key or a signed-in session. Scope: `webhook:admin`.

#### Parameters

POST /v1/webhooks parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Request body application/json, required

POST /v1/webhooks request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `description` | string or null | no |  |
| `event_types` | array of string or null | no | Empty or omitted: every event type. |
| `url` | string | yes |  |

#### Responses

POST /v1/webhooks responses

| Status | Meaning |
| --- | --- |
| `201` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

POST /v1/webhooks response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `created_at` | string | yes |  |
| `description` | string or null | yes |  |
| `event_types` | array of string | yes |  |
| `id` | string | yes |  |
| `previous_secret_expires_at` | string or null | yes |  |
| `secret` | string | yes | whsec_... Shown once. Verify webhook-signature with it. |
| `status` | string | yes |  |
| `updated_at` | string | yes |  |
| `url` | string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/webhooks \ -X POST \ -H "Authorization: Bearer ffa_test_..." \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com/webhooks/fax"}'`
```

### DELETE `/v1/webhooks/{webhook_id}`

Remove a destination

Credential: an API key or a signed-in session. Scope: `webhook:admin`.

#### Parameters

DELETE /v1/webhooks/{webhook_id} parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `webhook_id` | path | string | yes |  |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Responses

DELETE /v1/webhooks/{webhook_id} responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

DELETE /v1/webhooks/{webhook_id} response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `deleted` | boolean | no |  |
| `id` | string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/webhooks/WEBHOOK_ID \ -X DELETE \ -H "Authorization: Bearer ffa_test_..."`
```

### PATCH `/v1/webhooks/{webhook_id}`

Change url, filters or pause

Credential: an API key or a signed-in session. Scope: `webhook:admin`.

#### Parameters

PATCH /v1/webhooks/{webhook_id} parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `webhook_id` | path | string | yes |  |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Request body application/json, required

PATCH /v1/webhooks/{webhook_id} request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `description` | string or null | no |  |
| `event_types` | array of string or null | no |  |
| `status` | string (active, paused) or null | no |  |
| `url` | string or null | no |  |

#### Responses

PATCH /v1/webhooks/{webhook_id} responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

PATCH /v1/webhooks/{webhook_id} response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `created_at` | string | yes |  |
| `description` | string or null | yes |  |
| `event_types` | array of string | yes |  |
| `id` | string | yes |  |
| `previous_secret_expires_at` | string or null | yes |  |
| `status` | string | yes |  |
| `updated_at` | string | yes |  |
| `url` | string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/webhooks/WEBHOOK_ID \ -X PATCH \ -H "Authorization: Bearer ffa_test_..." \ -H "Content-Type: application/json" \ -d '{}'`
```

### POST `/v1/webhooks/{webhook_id}/rotate-secret`

New secret; the old one signs too for 24 hours

Credential: an API key or a signed-in session. Scope: `webhook:admin`.

#### Parameters

POST /v1/webhooks/{webhook_id}/rotate-secret parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `webhook_id` | path | string | yes |  |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Responses

POST /v1/webhooks/{webhook_id}/rotate-secret responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

POST /v1/webhooks/{webhook_id}/rotate-secret response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `created_at` | string | yes |  |
| `description` | string or null | yes |  |
| `event_types` | array of string | yes |  |
| `id` | string | yes |  |
| `previous_secret_expires_at` | string or null | yes |  |
| `secret` | string | yes | whsec_... Shown once. Verify webhook-signature with it. |
| `status` | string | yes |  |
| `updated_at` | string | yes |  |
| `url` | string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/webhooks/WEBHOOK_ID/rotate-secret \ -X POST \ -H "Authorization: Bearer ffa_test_..."`
```

### POST `/v1/webhooks/{webhook_id}/test`

Send a signed webhook.test event to this destination

Credential: an API key or a signed-in session. Scope: `webhook:admin`.

#### Parameters

POST /v1/webhooks/{webhook_id}/test parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `webhook_id` | path | string | yes |  |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Responses

POST /v1/webhooks/{webhook_id}/test responses

| Status | Meaning |
| --- | --- |
| `202` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

POST /v1/webhooks/{webhook_id}/test response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `correlation_id` | string or null | yes |  |
| `created_at` | string | yes |  |
| `data` | object | yes |  |
| `environment` | string (live, test) | yes |  |
| `fax_job_id` | string or null | yes |  |
| `id` | string | yes |  |
| `org_id` | string | yes |  |
| `project_id` | string or null | yes |  |
| `resource_id` | string | yes |  |
| `resource_type` | string | yes |  |
| `schema_version` | integer | yes |  |
| `type` | string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/webhooks/WEBHOOK_ID/test \ -X POST \ -H "Authorization: Bearer ffa_test_..."`
```
