TouricalDevelopers
Endpoints

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.

MethodPathScope
GET/api/v1/webhookswebhooks:read
POST/api/v1/webhookswebhooks:write
DELETE/api/v1/webhooks/{id}webhooks:write
POST/api/v1/webhooks/{id}/rotate-keywebhooks:write
GET/api/v1/webhooks/{id}/deliverieswebhooks:read
POST/api/v1/webhooks/{id}/deliveries/{deliveryId}/redeliverwebhooks:write
POST/api/v1/webhooks/testwebhooks:write

The WebhookSubscription object:

FieldTypeNotes
idstringOpaque subscription id.
urlstringEndpoint we POST events to. HTTPS only.
eventsstring[]Event types this subscription receives.
activebooleanFalse after a delete or auto-pause.
pausedAtISO 8601 | nullSet by the auto-pause guard (24 consecutive 4xx over ≥ 24h) or by a delete.
consecutiveFailuresintegerResets to 0 on a successful delivery.
lastSuccessAtISO 8601 | null
lastFailureAtISO 8601 | null
createdAtISO 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:

FieldTypeNotes
idstringDelivery id — the first half of Tourical-Request-Id.
eventIdstringMatches Tourical-Event-Id. Shared by redeliveries of the same event.
eventTypestring
statuspending | in_flight | success | dead_letter
attemptsintegerHTTP attempts made so far.
lastResponseStatusinteger | nullThe HTTP status your endpoint last answered with. The response body is not kept.
nextAttemptAtISO 8601 | null
deliveredAtISO 8601 | null
claimedAtISO 8601 | nullSet while a worker holds the row; part of the atomic claim that stops two workers sending the same delivery.
redeliveredFromIdstring | nullSet on rows created by redeliver.
createdAtISO 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.

GET/api/v1/webhooksTry it

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.

GET /api/v1/webhooks

POST /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

FieldTypeRequiredNotes
urlstringyesHTTPS only, ≤ 2048 chars, must resolve to a public IP at create time. Private / loopback / CGNAT addresses are rejected.
eventsstring[]yes1+ 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.

POST/api/v1/webhooksTry it

Returns 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/webhooks

DELETE /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

NameTypeRequiredNotes
idstringyesSubscription id.

Response 204 — empty body.

Errors — 401, 403 scope-required, 404 not-found.

DELETE/api/v1/webhooks/{id}Try it

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

idrequired
DELETE /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.

POST/api/v1/webhooks/{id}/rotate-keyTry it

Returns 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

idrequired
POST /api/v1/webhooks/{id}/rotate-key

GET /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

NameTypeNotes
statusstringOne of pending, in_flight, success, dead_letter.
limitinteger1–100, default 50.
cursorstringFrom the previous page's meta.cursor.

Response 200

{ "data": [ /* WebhookDelivery[] */ ], "meta": { "cursor": "…", "hasMore": true } }

Errors — 401, 403 scope-required, 404 not-found, 429.

GET/api/v1/webhooks/{id}/deliveriesTry it

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

idrequired

Query parameters

status
GET /api/v1/webhooks/{id}/deliveries

POST /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.

POST/api/v1/webhooks/{id}/deliveries/{deliveryId}/redeliverTry it

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

idrequired
deliveryIdrequired
POST /api/v1/webhooks/{id}/deliveries/{deliveryId}/redeliver

POST /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

FieldTypeRequiredNotes
subscriptionIdstringnoRestrict 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.

POST/api/v1/webhooks/testTry it

Fires 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

On this page