Webhook endpoints
Give this prompt to your AI agent. The agent does the task for you.
Read https://thirds.ai/docs/api-reference/webhooks and help me choose the right thirds.ai endpoint and write a request. Check the fields and credentials in this reference.
Manage webhook receivers, rotate secrets, check deliveries, and replay failed events for your account.
See all API resources or read API keys and requests.
Endpoints
testWebhook
POST /v1/webhooks/{webhook_id}/test
Send a signed test event
Queue one sample render.succeeded event through the normal delivery worker. It spends no credits and creates no render. A recorded result schedules no retry, and tests cannot be replayed. Recovery can resend an unfinished attempt with the same event ID. Disabled destinations can be tested. One test per destination can wait at a time; completed tests are limited to one per minute. The destination signing secrets and egress policy apply.
Authentication: bearer API key or browser session cookie with a CSRF token for writes. See authentication.
| Parameter | Location | Required | Rule |
|---|---|---|---|
webhook_id | path | Yes | The destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^wh_[0-9a-f]{32}$"} |
| Status | Meaning | Body |
|---|---|---|
| 200 | The test attempt finished. | application/json: WebhookTest |
| 202 | The test is still queued or in flight. Read the delivery log for its result before sending another. | application/json: WebhookTest |
| 401 | A valid session or API key is required. | application/json: ErrorEnvelope |
| 403 | The authenticated account is suspended. The code is account_suspended. | application/json: ErrorEnvelope |
| 404 | No destination with this ID belongs to this account. | application/json: ErrorEnvelope |
| 405 | The method is not allowed on this route. | application/json: ErrorEnvelope |
| 429 | The request limit was reached. Retry after the time in Retry-After. | application/json: ErrorEnvelope |
| 431 | The request has more than 64 headers or more than 32 KiB of header names and values. | application/json: ErrorEnvelope |
| 500 | The request failed internally. | application/json: ErrorEnvelope |
listWebhookDeliveries
GET /v1/webhooks/{webhook_id}/deliveries
Read recent webhook attempts
Return up to 20 newest attempts for this destination, in reverse time order. Attempts expire after 30 days. No request or response body is stored.
Authentication: bearer API key or browser session cookie with a CSRF token for writes. See authentication.
| Parameter | Location | Required | Rule |
|---|---|---|---|
webhook_id | path | Yes | The destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^wh_[0-9a-f]{32}$"} |
| Status | Meaning | Body |
|---|---|---|
| 200 | The latest attempts for this destination. | application/json: WebhookDeliveryList |
| 401 | A valid session or API key is required. | application/json: ErrorEnvelope |
| 403 | The authenticated account is suspended. The code is account_suspended. | application/json: ErrorEnvelope |
| 404 | No destination with this ID belongs to this account. | application/json: ErrorEnvelope |
| 405 | The method is not allowed on this route. | application/json: ErrorEnvelope |
| 429 | The request limit was reached. Retry after the time in Retry-After. | application/json: ErrorEnvelope |
| 431 | The request has more than 64 headers or more than 32 KiB of header names and values. | application/json: ErrorEnvelope |
| 500 | The request failed internally. | application/json: ErrorEnvelope |
createWebhook
POST /v1/webhooks
Register a webhook destination
Register one HTTPS destination for terminal render events. The URL must pass the same egress policy the platform applies to every outbound request — public HTTPS on port 443, no credentials, and a DNS answer set of public addresses only — and delivery rechecks all of it on every connection, so a name that later resolves privately is refused then too. The signing secret is returned in this response and never again. Every delivery carries a Thirds-Signature header of the form "t=<unix-seconds>,v1=<hex>": each v1 value is HMAC-SHA256 over the exact bytes "<t>.<body>" with one signing secret, so verify against the raw body before parsing it, and refuse a timestamp too far from your own clock to bound replays. During a rotation overlap the header carries two v1 values — the current secret's first, then the previous secret's — and a receiver accepts the delivery when any one value matches.
Authentication: bearer API key. See authentication.
Request body: application/json, NewWebhook.
| Status | Meaning | Body |
|---|---|---|
| 201 | The destination was registered and is enabled. This is the only response, with rotation's, that ever carries the signing secret. | application/json: WebhookWithSecret |
| 400 | The URL failed the egress policy — not HTTPS on port 443, malformed, carrying credentials, or resolving to a private, loopback, link-local, metadata, or reserved address — or the events list was empty, had duplicates, had more than three entries, or named an unknown event. The details entry carries the stable policy code that refused it. | application/json: ErrorEnvelope |
| 401 | The request did not carry a valid, active API key. | application/json: ErrorEnvelope |
| 403 | The authenticated account is suspended. The code is account_suspended. | application/json: ErrorEnvelope |
| 405 | The method is not allowed on this route. | application/json: ErrorEnvelope |
| 409 | The account already holds ten webhook destinations. Delete one before registering another. | application/json: ErrorEnvelope |
| 413 | The request body is larger than 16 KiB. | application/json: ErrorEnvelope |
| 415 | The request did not carry a JSON content type. | application/json: ErrorEnvelope |
| 431 | The request has more than 64 headers or more than 32 KiB of header names and values. | application/json: ErrorEnvelope |
| 500 | An internal error occurred. | application/json: ErrorEnvelope |
listWebhooks
GET /v1/webhooks
List webhook destinations
List every webhook destination of the authenticated account, most recently created first. The signing secret never appears here.
Authentication: bearer API key. See authentication.
| Status | Meaning | Body |
|---|---|---|
| 200 | The account's destinations. | application/json: WebhookList |
| 401 | The request did not carry a valid, active API key. | application/json: ErrorEnvelope |
| 403 | The authenticated account is suspended. The code is account_suspended. | application/json: ErrorEnvelope |
| 405 | The method is not allowed on this route. | application/json: ErrorEnvelope |
| 431 | The request has more than 64 headers or more than 32 KiB of header names and values. | application/json: ErrorEnvelope |
| 500 | An internal error occurred. | application/json: ErrorEnvelope |
getWebhook
GET /v1/webhooks/{webhook_id}
Read a webhook destination
Read one destination of the authenticated account, without its signing secret.
Authentication: bearer API key. See authentication.
| Parameter | Location | Required | Rule |
|---|---|---|---|
webhook_id | path | Yes | The destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^wh_[0-9a-f]{32}$"} |
| Status | Meaning | Body |
|---|---|---|
| 200 | The destination. | application/json: Webhook |
| 401 | The request did not carry a valid, active API key. | application/json: ErrorEnvelope |
| 403 | The authenticated account is suspended. The code is account_suspended. | application/json: ErrorEnvelope |
| 404 | No destination with this identifier belongs to the authenticated account. This is also the answer for an identifier that does not exist at all, so a request can never learn which one is true. | application/json: ErrorEnvelope |
| 405 | The method is not allowed on this route. | application/json: ErrorEnvelope |
| 431 | The request has more than 64 headers or more than 32 KiB of header names and values. | application/json: ErrorEnvelope |
| 500 | An internal error occurred. | application/json: ErrorEnvelope |
deleteWebhook
DELETE /v1/webhooks/{webhook_id}
Delete a webhook destination
Delete one destination of the authenticated account. Deliveries stop at once, and events not yet delivered to it are dropped.
Authentication: bearer API key. See authentication.
| Parameter | Location | Required | Rule |
|---|---|---|---|
webhook_id | path | Yes | The destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^wh_[0-9a-f]{32}$"} |
| Status | Meaning | Body |
|---|---|---|
| 204 | The destination is deleted. There is no body. | No body |
| 401 | The request did not carry a valid, active API key. | application/json: ErrorEnvelope |
| 403 | The authenticated account is suspended. The code is account_suspended. | application/json: ErrorEnvelope |
| 404 | No destination with this identifier belongs to the authenticated account. This is also the answer for an identifier that does not exist at all, so a request can never learn which one is true. | application/json: ErrorEnvelope |
| 405 | The method is not allowed on this route. | application/json: ErrorEnvelope |
| 431 | The request has more than 64 headers or more than 32 KiB of header names and values. | application/json: ErrorEnvelope |
| 500 | An internal error occurred. | application/json: ErrorEnvelope |
rotateWebhookSecret
POST /v1/webhooks/{webhook_id}/rotate
Rotate a webhook destination's signing secret
Mint a new signing secret for one destination. The new secret signs immediately and is returned in this response and never again. The previous secret keeps signing alongside it for 24 hours — every delivery in the overlap carries both signatures — so switch the receiver to the new secret inside that window. Rotating again during an overlap replaces the previous secret at once: only the last two secrets ever sign.
Authentication: bearer API key. See authentication.
| Parameter | Location | Required | Rule |
|---|---|---|---|
webhook_id | path | Yes | The destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^wh_[0-9a-f]{32}$"} |
| Status | Meaning | Body |
|---|---|---|
| 200 | The destination with its new secret. This is the only response, with creation's, that ever carries the signing secret. | application/json: WebhookWithSecret |
| 401 | The request did not carry a valid, active API key. | application/json: ErrorEnvelope |
| 403 | The authenticated account is suspended. The code is account_suspended. | application/json: ErrorEnvelope |
| 404 | No destination with this identifier belongs to the authenticated account. This is also the answer for an identifier that does not exist at all, so a request can never learn which one is true. | application/json: ErrorEnvelope |
| 405 | The method is not allowed on this route. | application/json: ErrorEnvelope |
| 431 | The request has more than 64 headers or more than 32 KiB of header names and values. | application/json: ErrorEnvelope |
| 500 | An internal error occurred. | application/json: ErrorEnvelope |
disableWebhook
POST /v1/webhooks/{webhook_id}/disable
Disable a webhook destination
Stop deliveries to one destination without deleting it. Disabling a destination that is already disabled returns the same answer again rather than an error, so a retried request is never rejected.
Authentication: bearer API key. See authentication.
| Parameter | Location | Required | Rule |
|---|---|---|---|
webhook_id | path | Yes | The destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^wh_[0-9a-f]{32}$"} |
| Status | Meaning | Body |
|---|---|---|
| 200 | The destination, now disabled by owner request. | application/json: Webhook |
| 401 | The request did not carry a valid, active API key. | application/json: ErrorEnvelope |
| 403 | The authenticated account is suspended. The code is account_suspended. | application/json: ErrorEnvelope |
| 404 | No destination with this identifier belongs to the authenticated account. This is also the answer for an identifier that does not exist at all, so a request can never learn which one is true. | application/json: ErrorEnvelope |
| 405 | The method is not allowed on this route. | application/json: ErrorEnvelope |
| 431 | The request has more than 64 headers or more than 32 KiB of header names and values. | application/json: ErrorEnvelope |
| 500 | An internal error occurred. | application/json: ErrorEnvelope |
enableWebhook
POST /v1/webhooks/{webhook_id}/enable
Enable a webhook destination
Resume deliveries to one destination, whether the owner disabled it or repeated delivery failures did, and reset its consecutive failure count. Enabling a destination that is already enabled returns the same answer again rather than an error, so a retried request is never rejected.
Authentication: bearer API key. See authentication.
| Parameter | Location | Required | Rule |
|---|---|---|---|
webhook_id | path | Yes | The destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^wh_[0-9a-f]{32}$"} |
| Status | Meaning | Body |
|---|---|---|
| 200 | The destination, enabled again. | application/json: Webhook |
| 401 | The request did not carry a valid, active API key. | application/json: ErrorEnvelope |
| 403 | The authenticated account is suspended. The code is account_suspended. | application/json: ErrorEnvelope |
| 404 | No destination with this identifier belongs to the authenticated account. This is also the answer for an identifier that does not exist at all, so a request can never learn which one is true. | application/json: ErrorEnvelope |
| 405 | The method is not allowed on this route. | application/json: ErrorEnvelope |
| 431 | The request has more than 64 headers or more than 32 KiB of header names and values. | application/json: ErrorEnvelope |
| 500 | An internal error occurred. | application/json: ErrorEnvelope |
replayWebhook
POST /v1/webhooks/{webhook_id}/replay
Replay a webhook destination's failed events
Return every event of one destination that ran out of delivery attempts to the queue with a fresh attempt budget. An event gets eight attempts per creation or replay, spread over roughly one day of growing backoff; after that it is failed and waits here. A replayed delivery carries the same event id and the same body as before, with a current signature and timestamp, so a receiver that stores event ids deduplicates it like any other retry. Replaying a destination with no failed events answers a count of zero rather than an error, so a retried request is never rejected.
Authentication: bearer API key. See authentication.
| Parameter | Location | Required | Rule |
|---|---|---|---|
webhook_id | path | Yes | The destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^wh_[0-9a-f]{32}$"} |
| Status | Meaning | Body |
|---|---|---|
| 200 | How many failed events returned to the queue. | application/json: WebhookReplay |
| 401 | The request did not carry a valid, active API key. | application/json: ErrorEnvelope |
| 403 | The authenticated account is suspended. The code is account_suspended. | application/json: ErrorEnvelope |
| 404 | No destination with this identifier belongs to the authenticated account. This is also the answer for an identifier that does not exist at all, so a request can never learn which one is true. | application/json: ErrorEnvelope |
| 405 | The method is not allowed on this route. | application/json: ErrorEnvelope |
| 431 | The request has more than 64 headers or more than 32 KiB of header names and values. | application/json: ErrorEnvelope |
| 500 | An internal error occurred. | application/json: ErrorEnvelope |
Schemas
WebhookTest
| Field | Required | Description and type |
|---|---|---|
status_code | Yes | {"type":["integer","null"],"minimum":200,"maximum":599} |
delivered | Yes | {"type":"boolean"} |
Schema rules: {"type":"object","additionalProperties":false}.
WebhookDelivery
| Field | Required | Description and type |
|---|---|---|
id | Yes | {"type":"string","pattern":"^wa_[0-9a-f]{32}$"} |
event | Yes | {"type":"string","enum":["render.succeeded","render.failed","render.cancelled"]} |
created_at | Yes | {"type":"string","format":"date-time"} |
status_code | Yes | Null when no HTTP status is received. {"type":["integer","null"],"minimum":200,"maximum":599} |
is_test | Yes | True for a sample sent by the test action. {"type":"boolean"} |
retry_count | Yes | {"type":"integer","minimum":0,"maximum":7} |
Schema rules: {"type":"object","additionalProperties":false}.
WebhookDeliveryList
| Field | Required | Description and type |
|---|---|---|
data | Yes | {"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/WebhookDelivery"}} — WebhookDelivery |
Schema rules: {"type":"object","additionalProperties":false}.
NewWebhook
What creating a webhook destination needs.
| Field | Required | Description and type |
|---|---|---|
url | Yes | The HTTPS URL deliveries are sent to. It must use port 443, carry no credentials, and resolve to public addresses only; private, loopback, link-local, metadata, and reserved destinations are refused, at registration and again on every delivery connection. {"type":"string","format":"uri","maxLength":2048} |
events | No | The terminal render events this destination receives. A body without this field selects every event. {"type":"array","minItems":1,"maxItems":3,"uniqueItems":true,"items":{"type":"string","enum":["render.cancelled","render.failed","render.succeeded"]}} |
Schema rules: {"type":"object","additionalProperties":false}.
WebhookFields
| Field | Required | Description and type |
|---|---|---|
id | Yes | The destination's public identifier: "wh_" followed by 32 lowercase hexadecimal characters. {"type":"string","pattern":"^wh_[0-9a-f]{32}$"} |
url | Yes | {"type":"string","format":"uri","maxLength":2048} |
events | Yes | The selected events, always sorted and distinct. {"type":"array","minItems":1,"maxItems":3,"uniqueItems":true,"items":{"type":"string","enum":["render.cancelled","render.failed","render.succeeded"]}} |
status | Yes | {"type":"string","enum":["enabled","disabled"]} |
disabled_reason | Yes | Why a disabled destination is disabled: the owner asked, or delivery failed repeatedly. Null while the destination is enabled. {"type":["string","null"],"enum":["owner_request","delivery_failure",null]} |
display_prefix | Yes | The first characters of the signing secret, enough to tell destinations apart in a list. {"type":"string"} |
last_delivery_at | Yes | Time of the most recent recorded attempt, including tests. {"type":["string","null"],"format":"date-time"} |
failure_count | Yes | Consecutive normal delivery failures. A success or enable resets it. Test events do not change it. {"type":"integer","minimum":0} |
created_at | Yes | {"type":"string","format":"date-time"} |
previous_secret_expires_at | Yes | Set while a rotation overlap is running: until this instant every delivery also carries a signature made with the previous secret. {"type":["string","null"],"format":"date-time"} |
Schema rules: {"type":"object"}.
Webhook
One webhook destination, without its signing secret.
Schema rules: {"allOf":[{"$ref":"#/components/schemas/WebhookFields"}],"unevaluatedProperties":false} — WebhookFields.
WebhookWithSecret
One webhook destination exactly as creation and rotation answer it. This is the only shape that ever carries the signing secret.
Schema rules: {"allOf":[{"$ref":"#/components/schemas/WebhookFields"},{"type":"object","properties":{"secret":{"type":"string","pattern":"^thirds_whsec_[0-9a-f]{64}$","description":"The full signing secret: \"thirds_whsec_\" followed by 64 lowercase hexadecimal characters. It is shown here once and never again; store it and verify every delivery's Thirds-Signature header with it."}},"required":["secret"]}],"unevaluatedProperties":false} — WebhookFields.
WebhookList
Every webhook destination the account holds. The cap is ten, so the list is never paged.
| Field | Required | Description and type |
|---|---|---|
data | Yes | {"type":"array","maxItems":10,"items":{"$ref":"#/components/schemas/Webhook"}} — Webhook |
Schema rules: {"type":"object","additionalProperties":false}.
WebhookReplay
What one replay request did.
| Field | Required | Description and type |
|---|---|---|
replayed | Yes | How many failed events returned to the queue with a fresh attempt budget. {"type":"integer","minimum":0} |
Schema rules: {"type":"object","additionalProperties":false}.
Shared errors, headers, and authentication schemes are in shared types and security.