Webhook endpoints
URLs that receive signed event deliveries.
Create
The create response returns signing_secret once. Store it for signature verification; retrieve, list, and update responses do not reveal it.
Request body
| Field | Type | Notes |
|---|---|---|
urlrequired | string | HTTPS URL that receives deliveries. Plain http, localhost, private-network and credential-bearing URLs are rejected with unsafe_webhook_url. |
description | string | |
subscribed_eventsrequired | string[] |
Response
| Field | Type | Notes |
|---|---|---|
idrequired | string | |
objectrequired | "webhook_endpoint" | |
urlrequired | string | |
descriptionrequired | string | null | |
subscribed_eventsrequired | string[] | |
statusrequired | "active" | "disabled" | |
probation | boolean | True after breaker recovery until the first successful real delivery; diagnostic probes cannot end probation. Current reads always emit it. |
disabled_reasonrequired | string | null | |
disabled_detailrequired | object | null | |
last_failure_atrequired | string<ISO-8601> | null | |
last_success_atrequired | string<ISO-8601> | null | |
environmentrequired | "sandbox" | "production" | |
created_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
updated_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
signing_secretrequired | string |
List
Query
| Field | Type | Notes |
|---|---|---|
limitrequired | integer | Maximum number of items to return. Defaults to 20; maximum 100. range 1..100 |
starting_after | string | Opaque cursor from the previous response next_cursor. |
Response
| Field | Type | Notes |
|---|---|---|
objectrequired | "list" | |
datarequired | object[] | |
has_morerequired | boolean | |
next_cursorrequired | string | null |
Retrieve
Every endpoint response includes nullable disabled_detail. An auto-disabled endpoint reports its triggering last_status_code (or null if no response arrived), failure_class, and probation_retrip: true on a probation re-trip; active, manually disabled, and legacy endpoints return null. It never includes breaker counters, internal events, or a signing secret.
Response
| Field | Type | Notes |
|---|---|---|
idrequired | string | |
objectrequired | "webhook_endpoint" | |
urlrequired | string | |
descriptionrequired | string | null | |
subscribed_eventsrequired | string[] | |
statusrequired | "active" | "disabled" | |
probation | boolean | True after breaker recovery until the first successful real delivery; diagnostic probes cannot end probation. Current reads always emit it. |
disabled_reasonrequired | string | null | |
disabled_detailrequired | object | null | |
last_failure_atrequired | string<ISO-8601> | null | |
last_success_atrequired | string<ISO-8601> | null | |
environmentrequired | "sandbox" | "production" | |
created_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
updated_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
Update
Setting status to active re-enables an endpoint the auto-disable breaker turned off after 20 consecutive failed deliveries, clearing its consecutive-failure counter, disabled_reason, and disabled_detail; this route is the only way to re-enable one. It does not rotate the signing secret, replay terminal events, or send a test delivery. Re-enabling after a breaker trip sets probation to true until the first real delivery success. During probation it re-trips after three consecutive real delivery failures. The first successful real delivery clears probation and restores the normal twenty-failure threshold. A failed test never blocks this PATCH.
Request body
| Field | Type | Notes |
|---|---|---|
url | string | HTTPS URL that receives deliveries. Plain http, localhost, private-network and credential-bearing URLs are rejected with unsafe_webhook_url. |
description | string | null | |
subscribed_events | string[] | |
status | "active" | "disabled" |
Response
| Field | Type | Notes |
|---|---|---|
idrequired | string | |
objectrequired | "webhook_endpoint" | |
urlrequired | string | |
descriptionrequired | string | null | |
subscribed_eventsrequired | string[] | |
statusrequired | "active" | "disabled" | |
probation | boolean | True after breaker recovery until the first successful real delivery; diagnostic probes cannot end probation. Current reads always emit it. |
disabled_reasonrequired | string | null | |
disabled_detailrequired | object | null | |
last_failure_atrequired | string<ISO-8601> | null | |
last_success_atrequired | string<ISO-8601> | null | |
environmentrequired | "sandbox" | "production" | |
created_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
updated_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
Rotate signing secret
Returns the new signing_secret once. Existing endpoint details continue to hide signing secrets.
Response
| Field | Type | Notes |
|---|---|---|
idrequired | string | |
objectrequired | "webhook_endpoint" | |
urlrequired | string | |
descriptionrequired | string | null | |
subscribed_eventsrequired | string[] | |
statusrequired | "active" | "disabled" | |
probation | boolean | True after breaker recovery until the first successful real delivery; diagnostic probes cannot end probation. Current reads always emit it. |
disabled_reasonrequired | string | null | |
disabled_detailrequired | object | null | |
last_failure_atrequired | string<ISO-8601> | null | |
last_success_atrequired | string<ISO-8601> | null | |
environmentrequired | "sandbox" | "production" | |
created_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
updated_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
signing_secretrequired | string |
Test delivery
Sends an empty, idempotent direct probe to one active endpoint and returns a durable pending receipt. Store webhook_event_id, then retrieve that event to inspect its delivery status and first-attempt evidence.
A missing, deleted, or out-of-scope endpoint returns 404 webhook_endpoint_not_found. A disabled endpoint returns 409 webhook_endpoint_disabled; repair the receiver and re-enable it with PATCH /v1/webhook-endpoints/{id} and status: active. Each endpoint allows five test requests per minute; on 429 rate_limited, honor Retry-After and error.retry_after_ms.
202 response
| Field | Type | Notes |
|---|---|---|
objectrequired | "webhook_test" | |
webhook_event_idrequired | string | |
webhook_endpoint_idrequired | string | |
statusrequired | "pending" |
Delete
List events
Query
| Field | Type | Notes |
|---|---|---|
limitrequired | integer | Maximum number of items to return. Defaults to 20; maximum 100. range 1..100 |
starting_after | string | Opaque cursor from the previous response next_cursor. |
event_type | string | len 1..∞ |
event_types | string | string[] | |
webhook_endpoint_id | string | |
status | "pending" | "delivered" | "failed" |
Response
| Field | Type | Notes |
|---|---|---|
objectrequired | "list" | |
datarequired | object[] | |
has_morerequired | boolean | |
next_cursorrequired | string | null |
Listen for events
Polls customer-visible events oldest first for local development companions and conformance probes. Sandbox keys can call directly. Production keys must create a short-lived listen session and pass its listen_session_id; production reads include only events with created_at >= session.created_at and created_at < session.expires_at. The route supports event_type or event_types, bounded long polling with wait_ms, and standard 429 rate-limit errors with Retry-After. Use limit to cap each page, pass the previous next_cursor as cursor, and use start_at=tail when the first poll should skip existing events. Pass webhook_endpoint_id to attach signature_preview, which includes the exact raw body, timestamp, signature, and Tyxter headers generated with that endpoint secret.
For message.received interactive replies, each listen item stores the webhook envelope under payload. Read button/list selections at payload.data.content.interactive.button_reply or payload.data.content.interactive.list_reply. Sender and recipient live at payload.data.sender and payload.data.recipient; there is no top-level payload.from.
Query
| Field | Type | Notes |
|---|---|---|
limitrequired | integer | Maximum number of events to return per poll. Defaults to 20; maximum 100. range 1..100 |
cursor | string | Opaque cursor from the previous listen response next_cursor. |
start_atrequired | "oldest" | "tail" | Initial listen position when cursor is omitted. "oldest" replays retained events; "tail" starts after the current stream tail. |
event_type | string | len 1..∞ |
event_types | string | string[] | |
wait_msrequired | integer | Long-poll wait budget in milliseconds. Defaults to 0; maximum 25000. range 0..25000 |
webhook_endpoint_id | string | Optional endpoint id used to filter listen events to that endpoint subscription and attach a signature_preview computed with that endpoint signing secret. |
listen_session_id | string | Required for production API keys. The owning API key creates, retains, and passes the session id. A recoverable conflict requires error.details.active_session.{id, expires_at, created_at}; only that owner may reuse and poll the id outside grace or renew with 30 seconds or less remaining. Renewal atomically disables the old session id, and the owner must switch polling to the returned 201 session id. A sibling must not poll it or loop POST, and waits until expires_at or coordinates with the owner to disable. |
Response
| Field | Type | Notes |
|---|---|---|
objectrequired | "webhook_event_listen" | |
datarequired | object[] | |
has_morerequired | boolean | |
next_cursorrequired | string | null | |
next_poll_after_ms | integer | range 0..9007199254740991 |
Create listen session
Production-only diagnostic lease for temporary no-public-URL polling. Sessions default to five minutes, cannot exceed five minutes, are rate-limited, and should be disabled when the probe finishes. Production listen sessions accept a requested TTL from 30 to 300 seconds and default to five minutes, which is also the maximum. Only the owning API key retains and passes its listen_session_id while polling. They expose only events with created_at >= session.created_at and < session.expires_at and are not a durable or gapless transport. If POST /v1/webhook-events/listen-sessions returns 409 webhook_listen_session_active_exists, the required recovery metadata is error.details.active_session.{id, expires_at, created_at}. Outside grace, only the owner may reuse and poll that id; with 30 seconds or less remaining, only the owner may POST again and must use the returned new id. The old id is disabled by that replacement. A sibling must not poll that id or loop POST: it waits until error.details.active_session.expires_at or coordinates with the owner to disable. The conflict does not consume a create rate-limit token.
Request body
| Field | Type | Notes |
|---|---|---|
ttl_secondsrequired | integer | Requested listen-session TTL in seconds. Minimum 30; default and maximum 300. range 30..300 |
reason | string | len 1..200 |
Response
| Field | Type | Notes |
|---|---|---|
idrequired | string | |
objectrequired | "webhook_listen_session" | |
environmentrequired | "production" | |
statusrequired | "active" | "disabled" | "expired" | |
expires_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
disabled_atrequired | string<ISO-8601> | null | |
created_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
trace_idrequired | string | |
poll_endpointrequired | "/v1/webhook-events/listen" | |
retrieve_endpointrequired | "/v1/webhook-events/listen/:outbox_event_id" |
Disable listen session
Response
| Field | Type | Notes |
|---|---|---|
idrequired | string | |
objectrequired | "webhook_listen_session" | |
environmentrequired | "production" | |
statusrequired | "active" | "disabled" | "expired" | |
expires_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
disabled_atrequired | string<ISO-8601> | null | |
created_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
trace_idrequired | string | |
poll_endpointrequired | "/v1/webhook-events/listen" | |
retrieve_endpointrequired | "/v1/webhook-events/listen/:outbox_event_id" |
Retrieve listen event
Query
| Field | Type | Notes |
|---|---|---|
webhook_endpoint_id | string | Optional endpoint id used to attach a signature_preview computed with that endpoint signing secret. |
listen_session_id | string | Required for production API keys. The owning API key retains, reuses, and passes its id. A recoverable conflict requires error.details.active_session.{id, expires_at, created_at}; only that owner may reuse and poll it outside grace or renew with 30 seconds or less remaining. Renewal atomically disables the old session id, and the owner must switch polling to the returned 201 session id. A sibling must not poll it or loop POST, and waits until expires_at or coordinates with the owner to disable. The requested outbox event must have created_at >= session.created_at and < session.expires_at. |
Response
| Field | Type | Notes |
|---|---|---|
idrequired | string | |
objectrequired | "webhook_event" | |
endpoint_idrequired | string | null | |
typerequired | string | |
source_typerequired | string | |
source_idrequired | string | |
payloadrequired | unknown | null | The delivered event body. Null once contact erasure or the retention sweep cleared the stored payload — the event log row survives, its content does not. |
statusrequired | "pending" | "delivered" | "failed" | |
trace_idrequired | string | |
created_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
attemptsrequired | object[] | |
signature_preview | object |
Retrieve event
Response
| Field | Type | Notes |
|---|---|---|
idrequired | string | |
objectrequired | "webhook_event" | |
endpoint_idrequired | string | null | |
typerequired | string | |
source_typerequired | string | |
source_idrequired | string | |
payloadrequired | unknown | null | The delivered event body. Null once contact erasure or the retention sweep cleared the stored payload — the event log row survives, its content does not. |
statusrequired | "pending" | "delivered" | "failed" | |
trace_idrequired | string | |
created_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
attemptsrequired | object[] | |
signature_preview | object |
Resend event
Response
| Field | Type | Notes |
|---|---|---|
idrequired | string | |
statusrequired | "pending" | "succeeded" | "failed" | |
attemptrequired | integer | range 1..9007199254740991 |
status_coderequired | integer | null | |
failure_class | "auth_rejected" | "server_error" | "unreachable" | "timeout" | null | Null for pending, successful, historical or unclassified attempts; current reads always emit it. |
response_body_redactedrequired | string | null | |
error_messagerequired | string | null | |
next_retry_atrequired | string<ISO-8601> | null | |
created_atrequired | string<ISO-8601> | pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ |
Bulk resend events
Request body
| Field | Type | Notes |
|---|---|---|
webhook_event_ids | string[] | |
event_type | string | len 1..∞ |
status | "pending" | "delivered" | "failed" | |
limit | integer | range 1..100 |
Response
| Field | Type | Notes |
|---|---|---|
objectrequired | "webhook_bulk_resend" | |
requestedrequired | integer | range 0..9007199254740991 |
enqueuedrequired | integer | range 0..9007199254740991 |
skippedrequired | integer | range 0..9007199254740991 |
attemptsrequired | object[] |