Webhooks API reference
Updated
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.
- 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-deliveriesDelivery attempts (no payload secrets)POST /v1/webhook-deliveries/{delivery_id}/replayReplay the event with the same event idGET /v1/webhooksList webhooksPOST /v1/webhooksRegister a destination (secret shown once)DELETE /v1/webhooks/{webhook_id}Remove a destinationPATCH /v1/webhooks/{webhook_id}Change url, filters or pausePOST /v1/webhooks/{webhook_id}/rotate-secretNew secret; the old one signs too for 24 hoursPOST /v1/webhooks/{webhook_id}/testSend 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 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 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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_..."