API reference

Webhook endpoints

Use with your agent

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.

ParameterLocationRequiredRule
webhook_idpathYesThe destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^wh_[0-9a-f]{32}$"}
StatusMeaningBody
200The test attempt finished.application/json: WebhookTest
202The test is still queued or in flight. Read the delivery log for its result before sending another.application/json: WebhookTest
401A valid session or API key is required.application/json: ErrorEnvelope
403The authenticated account is suspended. The code is account_suspended.application/json: ErrorEnvelope
404No destination with this ID belongs to this account.application/json: ErrorEnvelope
405The method is not allowed on this route.application/json: ErrorEnvelope
429The request limit was reached. Retry after the time in Retry-After.application/json: ErrorEnvelope
431The request has more than 64 headers or more than 32 KiB of header names and values.application/json: ErrorEnvelope
500The 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.

ParameterLocationRequiredRule
webhook_idpathYesThe destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^wh_[0-9a-f]{32}$"}
StatusMeaningBody
200The latest attempts for this destination.application/json: WebhookDeliveryList
401A valid session or API key is required.application/json: ErrorEnvelope
403The authenticated account is suspended. The code is account_suspended.application/json: ErrorEnvelope
404No destination with this ID belongs to this account.application/json: ErrorEnvelope
405The method is not allowed on this route.application/json: ErrorEnvelope
429The request limit was reached. Retry after the time in Retry-After.application/json: ErrorEnvelope
431The request has more than 64 headers or more than 32 KiB of header names and values.application/json: ErrorEnvelope
500The 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.

StatusMeaningBody
201The destination was registered and is enabled. This is the only response, with rotation's, that ever carries the signing secret.application/json: WebhookWithSecret
400The 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
401The request did not carry a valid, active API key.application/json: ErrorEnvelope
403The authenticated account is suspended. The code is account_suspended.application/json: ErrorEnvelope
405The method is not allowed on this route.application/json: ErrorEnvelope
409The account already holds ten webhook destinations. Delete one before registering another.application/json: ErrorEnvelope
413The request body is larger than 16 KiB.application/json: ErrorEnvelope
415The request did not carry a JSON content type.application/json: ErrorEnvelope
431The request has more than 64 headers or more than 32 KiB of header names and values.application/json: ErrorEnvelope
500An 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.

StatusMeaningBody
200The account's destinations.application/json: WebhookList
401The request did not carry a valid, active API key.application/json: ErrorEnvelope
403The authenticated account is suspended. The code is account_suspended.application/json: ErrorEnvelope
405The method is not allowed on this route.application/json: ErrorEnvelope
431The request has more than 64 headers or more than 32 KiB of header names and values.application/json: ErrorEnvelope
500An 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.

ParameterLocationRequiredRule
webhook_idpathYesThe destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^wh_[0-9a-f]{32}$"}
StatusMeaningBody
200The destination.application/json: Webhook
401The request did not carry a valid, active API key.application/json: ErrorEnvelope
403The authenticated account is suspended. The code is account_suspended.application/json: ErrorEnvelope
404No 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
405The method is not allowed on this route.application/json: ErrorEnvelope
431The request has more than 64 headers or more than 32 KiB of header names and values.application/json: ErrorEnvelope
500An 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.

ParameterLocationRequiredRule
webhook_idpathYesThe destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^wh_[0-9a-f]{32}$"}
StatusMeaningBody
204The destination is deleted. There is no body.No body
401The request did not carry a valid, active API key.application/json: ErrorEnvelope
403The authenticated account is suspended. The code is account_suspended.application/json: ErrorEnvelope
404No 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
405The method is not allowed on this route.application/json: ErrorEnvelope
431The request has more than 64 headers or more than 32 KiB of header names and values.application/json: ErrorEnvelope
500An 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.

ParameterLocationRequiredRule
webhook_idpathYesThe destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^wh_[0-9a-f]{32}$"}
StatusMeaningBody
200The destination with its new secret. This is the only response, with creation's, that ever carries the signing secret.application/json: WebhookWithSecret
401The request did not carry a valid, active API key.application/json: ErrorEnvelope
403The authenticated account is suspended. The code is account_suspended.application/json: ErrorEnvelope
404No 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
405The method is not allowed on this route.application/json: ErrorEnvelope
431The request has more than 64 headers or more than 32 KiB of header names and values.application/json: ErrorEnvelope
500An 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.

ParameterLocationRequiredRule
webhook_idpathYesThe destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^wh_[0-9a-f]{32}$"}
StatusMeaningBody
200The destination, now disabled by owner request.application/json: Webhook
401The request did not carry a valid, active API key.application/json: ErrorEnvelope
403The authenticated account is suspended. The code is account_suspended.application/json: ErrorEnvelope
404No 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
405The method is not allowed on this route.application/json: ErrorEnvelope
431The request has more than 64 headers or more than 32 KiB of header names and values.application/json: ErrorEnvelope
500An 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.

ParameterLocationRequiredRule
webhook_idpathYesThe destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^wh_[0-9a-f]{32}$"}
StatusMeaningBody
200The destination, enabled again.application/json: Webhook
401The request did not carry a valid, active API key.application/json: ErrorEnvelope
403The authenticated account is suspended. The code is account_suspended.application/json: ErrorEnvelope
404No 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
405The method is not allowed on this route.application/json: ErrorEnvelope
431The request has more than 64 headers or more than 32 KiB of header names and values.application/json: ErrorEnvelope
500An 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.

ParameterLocationRequiredRule
webhook_idpathYesThe destination's public identifier, such as "wh_1f8b3c7d5e2a49061f8b3c7d5e2a4906". {"type":"string","pattern":"^wh_[0-9a-f]{32}$"}
StatusMeaningBody
200How many failed events returned to the queue.application/json: WebhookReplay
401The request did not carry a valid, active API key.application/json: ErrorEnvelope
403The authenticated account is suspended. The code is account_suspended.application/json: ErrorEnvelope
404No 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
405The method is not allowed on this route.application/json: ErrorEnvelope
431The request has more than 64 headers or more than 32 KiB of header names and values.application/json: ErrorEnvelope
500An internal error occurred.application/json: ErrorEnvelope

Schemas

WebhookTest

FieldRequiredDescription and type
status_codeYes{"type":["integer","null"],"minimum":200,"maximum":599}
deliveredYes{"type":"boolean"}

Schema rules: {"type":"object","additionalProperties":false}.

WebhookDelivery

FieldRequiredDescription and type
idYes{"type":"string","pattern":"^wa_[0-9a-f]{32}$"}
eventYes{"type":"string","enum":["render.succeeded","render.failed","render.cancelled"]}
created_atYes{"type":"string","format":"date-time"}
status_codeYesNull when no HTTP status is received. {"type":["integer","null"],"minimum":200,"maximum":599}
is_testYesTrue for a sample sent by the test action. {"type":"boolean"}
retry_countYes{"type":"integer","minimum":0,"maximum":7}

Schema rules: {"type":"object","additionalProperties":false}.

WebhookDeliveryList

FieldRequiredDescription and type
dataYes{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/WebhookDelivery"}}WebhookDelivery

Schema rules: {"type":"object","additionalProperties":false}.

NewWebhook

What creating a webhook destination needs.

FieldRequiredDescription and type
urlYesThe 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}
eventsNoThe 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

FieldRequiredDescription and type
idYesThe destination's public identifier: "wh_" followed by 32 lowercase hexadecimal characters. {"type":"string","pattern":"^wh_[0-9a-f]{32}$"}
urlYes{"type":"string","format":"uri","maxLength":2048}
eventsYesThe selected events, always sorted and distinct. {"type":"array","minItems":1,"maxItems":3,"uniqueItems":true,"items":{"type":"string","enum":["render.cancelled","render.failed","render.succeeded"]}}
statusYes{"type":"string","enum":["enabled","disabled"]}
disabled_reasonYesWhy 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_prefixYesThe first characters of the signing secret, enough to tell destinations apart in a list. {"type":"string"}
last_delivery_atYesTime of the most recent recorded attempt, including tests. {"type":["string","null"],"format":"date-time"}
failure_countYesConsecutive normal delivery failures. A success or enable resets it. Test events do not change it. {"type":"integer","minimum":0}
created_atYes{"type":"string","format":"date-time"}
previous_secret_expires_atYesSet 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.

FieldRequiredDescription and type
dataYes{"type":"array","maxItems":10,"items":{"$ref":"#/components/schemas/Webhook"}}Webhook

Schema rules: {"type":"object","additionalProperties":false}.

WebhookReplay

What one replay request did.

FieldRequiredDescription and type
replayedYesHow 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.

Ready to make your first file?

Start in the playground with a gallery template, or get an API key and send your first request from your own terminal.