Webhook subscriptions
Manage outbound webhook subscriptions, rotate signing keys, inspect and redeliver deliveries.
This page covers only the management endpoints. For the wire format and event payloads see Webhooks.
| Method | Path | Scope |
|---|---|---|
| GET | /api/v1/webhooks | webhooks:read |
| POST | /api/v1/webhooks | webhooks:write |
| DELETE | /api/v1/webhooks/{id} | webhooks:write |
| POST | /api/v1/webhooks/{id}/rotate-key | webhooks:write |
| GET | /api/v1/webhooks/{id}/deliveries | webhooks:read |
| POST | /api/v1/webhooks/{id}/deliveries/{deliveryId}/redeliver | webhooks:write |
| POST | /api/v1/webhooks/test | webhooks:write |
The WebhookSubscription object:
| Field | Type | Notes |
|---|---|---|
id | string | Opaque subscription id. |
url | string | Endpoint we POST events to. HTTPS only. |
events | string[] | Event types this subscription receives. |
active | boolean | False after a delete or auto-pause. |
pausedAt | ISO 8601 | null | Set by the auto-pause guard (24 consecutive 4xx over ≥ 24h) or by a delete. |
consecutiveFailures | integer | Resets to 0 on a successful delivery. |
lastSuccessAt | ISO 8601 | null | |
lastFailureAt | ISO 8601 | null | |
createdAt | ISO 8601 |
The signingKey field is never returned by GET. It's surfaced exactly once in the response of a successful POST (create or rotate).
The WebhookDelivery object:
| Field | Type | Notes |
|---|---|---|
id | string | Delivery id — the first half of Tourical-Request-Id. |
eventId | string | Matches Tourical-Event-Id. Shared by redeliveries of the same event. |
eventType | string | |
status | pending | in_flight | success | dead_letter | |
attempts | integer | HTTP attempts made so far. |
lastResponseStatus | integer | null | The HTTP status your endpoint last answered with. The response body is not kept. |
nextAttemptAt | ISO 8601 | null | |
deliveredAt | ISO 8601 | null | |
claimedAt | ISO 8601 | null | Set while a worker holds the row; part of the atomic claim that stops two workers sending the same delivery. |
redeliveredFromId | string | null | Set on rows created by redeliver. |
createdAt | ISO 8601 |
GET /api/v1/webhooks
List webhook subscriptions for the tenant.
Required scope — webhooks:read
Response 200
{ "data": [ /* WebhookSubscription[] */ ] }Errors — 401, 403 (scope-required, or plan-required when the plan has no API access), 429.
/api/v1/webhooksTry itRequests are proxied through this docs site so CORS doesn't block the call. Use a sandbox token if you don't want test calls hitting live data.
GET /api/v1/webhooksPOST /api/v1/webhooks
Create a webhook subscription. The plaintext signingKey is returned exactly once in the response. Lose it and you rotate it (below).
Required scope — webhooks:write
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
url | string | yes | HTTPS only, ≤ 2048 chars, must resolve to a public IP at create time. Private / loopback / CGNAT addresses are rejected. |
events | string[] | yes | 1+ entries from the event catalog. webhook.test is not subscribable — every active subscription receives it. |
Response 201
{
"data": /* WebhookSubscription */,
"signingKey": "zyXBHGFEDCBA9876…"
}The signing key is the value passed to your verifier — see Signing.
Errors — 401; 403 for all three refusals — scope-required, forbidden with code: "feature_off_for_tier" (webhooks are not on this plan) and forbidden with code: "webhook_quota_exceeded" (the per-plan endpoint cap is full); 422 validation-failed (bad URL shape, unknown event type, non-HTTPS, private IP); 429.
/api/v1/webhooksTry itReturns the plaintext signing key ONCE. Store it immediately.
Requests are proxied through this docs site so CORS doesn't block the call. Use a sandbox token if you don't want test calls hitting live data.
POST /api/v1/webhooksDELETE /api/v1/webhooks/{id}
Soft-delete a subscription. We set active=false and pausedAt=now(); the row and its delivery history are retained.
Required scope — webhooks:write
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
id | string | yes | Subscription id. |
Response 204 — empty body.
Errors — 401, 403 scope-required, 404 not-found.
/api/v1/webhooks/{id}Try itRequests are proxied through this docs site so CORS doesn't block the call. Use a sandbox token if you don't want test calls hitting live data.
Path parameters
idrequiredDELETE /api/v1/webhooks/{id}POST /api/v1/webhooks/{id}/rotate-key
Mint a new signing key. For the next 24 hours every delivery is signed with both keys (t=…,v1=<new>,v1=<old>), so you can roll your verifier over without dropping events. After the window the old key stops signing.
Required scope — webhooks:write
Response 200
{
"data": /* WebhookSubscription */,
"signingKey": "newPlaintext…",
"legacySigningKeyExpiresAt": "2026-05-27T12:00:00.000Z"
}Errors — 401, 403 scope-required, 404 not-found, 429.
/api/v1/webhooks/{id}/rotate-keyTry itReturns the NEW plaintext signing key once. The previous key co-signs for 24h.
Requests are proxied through this docs site so CORS doesn't block the call. Use a sandbox token if you don't want test calls hitting live data.
Path parameters
idrequiredPOST /api/v1/webhooks/{id}/rotate-keyGET /api/v1/webhooks/{id}/deliveries
List delivery rows for a subscription, newest first. Cursor-paginated like every list endpoint: pass limit and cursor, and read the next cursor back out of meta.
Required scope — webhooks:read
Query parameters
| Name | Type | Notes |
|---|---|---|
status | string | One of pending, in_flight, success, dead_letter. |
limit | integer | 1–100, default 50. |
cursor | string | From the previous page's meta.cursor. |
Response 200
{ "data": [ /* WebhookDelivery[] */ ], "meta": { "cursor": "…", "hasMore": true } }Errors — 401, 403 scope-required, 404 not-found, 429.
/api/v1/webhooks/{id}/deliveriesTry itRequests are proxied through this docs site so CORS doesn't block the call. Use a sandbox token if you don't want test calls hitting live data.
Path parameters
idrequiredQuery parameters
statusGET /api/v1/webhooks/{id}/deliveriesPOST /api/v1/webhooks/{id}/deliveries/{deliveryId}/redeliver
Queue a fresh delivery of the same event. Creates a new delivery row with the same Tourical-Event-Id; the original row is untouched. Safe for consumers that dedupe on the event id.
Required scope — webhooks:write
Response 202
{ "data": { "deliveryId": "whd_new…", "eventId": "7d3f0e2c-…", "status": "pending" } }Errors — 401, 403 scope-required, 404 not-found (subscription or delivery outside your tenant), 409 conflict with code: "in_flight" (an attempt is running right now) or code: "subscription_inactive" (paused or deleted — re-enable first), 429.
/api/v1/webhooks/{id}/deliveries/{deliveryId}/redeliverTry itRequests are proxied through this docs site so CORS doesn't block the call. Use a sandbox token if you don't want test calls hitting live data.
Path parameters
idrequireddeliveryIdrequiredPOST /api/v1/webhooks/{id}/deliveries/{deliveryId}/redeliverPOST /api/v1/webhooks/test
Fire a synthetic webhook.test event so you can verify your signature handling end-to-end. The test payload is shaped identically to a real one (see the catalog).
Required scope — webhooks:write
Request body — optional
| Field | Type | Required | Notes |
|---|---|---|---|
subscriptionId | string | no | Restrict the test fire-off to one subscription. Omitted, it fires to every active subscription regardless of its events list. |
Response 200
{
"data": {
"eventId": "7d3f0e2c-9aa1-4f2c-bd56-1c4a8e4f0b91",
"deliveries": 1
}
}deliveries is the number of delivery rows created. Follow the outcome with GET /v1/webhooks/{id}/deliveries or in the dashboard.
Errors — 401, 403 (scope-required, or plan-required when the plan has no API access), 404 not-found (unknown subscriptionId, or one outside your tenant), 422 validation-failed, 429.
/api/v1/webhooks/testTry itFires a synthetic webhook.test event. Optionally restrict to one subscription id.
Requests are proxied through this docs site so CORS doesn't block the call. Use a sandbox token if you don't want test calls hitting live data.
POST /api/v1/webhooks/test
