Skip to content

Webhook endpoints ​

The endpoints your organization's webhooks are delivered to — the same ones Manage → Webhooks edits. Every rule the dashboard applies applies here: the URL's checks, the template's fields, the filters. A change made here shows there at once, and the other way round.

RouteScopeDoes
GET /api/public/v1/webhookswebhook:readA page of endpoints, newest first
POST /api/public/v1/webhookswebhook:writeCreates an endpoint, with its secret
GET /api/public/v1/webhooks/{id}webhook:readOne endpoint
PATCH /api/public/v1/webhooks/{id}webhook:writeChanges its settings, or switches it
DELETE /api/public/v1/webhooks/{id}webhook:writeDeletes it
POST /api/public/v1/webhooks/{id}/rotate-secretwebhook:writeA new signing secret
POST /api/public/v1/webhooks/{id}/testwebhook:writeSends a test delivery and waits
GET /api/public/v1/webhooks/{id}/deliverieswebhook:readA page of its delivery log, newest first
PropertyValue
Org kindSeller organizations only
Cost1 token per request
BodyJSON, at most 64 KiB, on POST and PATCH of an endpoint; none elsewhere

webhook:read is in the default scope set; webhook:write must be granted explicitly. Events are emitted by the organization whose calls they describe — the seller side — so a buyer-only organization gets 403.

The endpoint object ​

FieldTypeNullableExample
idstring (uuid)No"eeeeeeee-1111-4111-8111-111111111111"
urlstringNo"https://hooks.example.com/dialcru"
methodstringNo"POST"
descriptionstringYes"CRM intake"
event_typestringNo"call_outcome.submitted"
subtypesarrayYes["interested", "callback"]
campaign_idsarrayYes["7e8f9a0b-1c2d-4e3f-a4b5-c6d7e8f9a0b1"]
headersarrayNosee below
body_templatestringNo"{\"status\": {{ call.status | json }}}"
schedule_templatestringYes"{{ call.ended_at }}"
content_typestringNo"application/json"
enabledbooleanNotrue
disabled_reasonstringYes"failures"
disabled_atstring (ISO)Yes"2026-11-02T09:00:00.000Z"
consecutive_failuresnumberNo0
secret_hintstringNo"…a1B2"
previous_secret_expires_atstring (ISO)Yes"2026-11-03T09:00:00.000Z"
created_atstring (ISO)No"2026-11-01T09:00:00.000Z"
updated_atstring (ISO)No"2026-11-02T09:00:00.000Z"
  • event_type is one event an endpoint subscribes to — see Events for the ids, their subtypes and their payloads. method is GET, POST, PUT, PATCH or DELETE.
  • subtypes narrows the event by its subtype dimension (a call's status, an outcome's disposition); campaign_ids by campaign. null is every — including subtypes and campaigns added later. An event that belongs to no campaign always reads campaign_ids: null.
  • body_template, schedule_template and content_type are the template the body is rendered from, the optional delivery time, and the body's media type.
  • disabled_reason is why an endpoint is off: manual, failures (20 in a row) or gone (it answered 410). See When we stop sending.
  • secret_hint is the signing secret's last four characters. The secret itself is shown once, when it is made, and never again.
  • previous_secret_expires_at is set after a rotation, while the old secret still signs beside the new one.

Each header:

json
[
  { "id": "…", "name": "X-Source", "secret": false, "value": "dialcru" },
  { "id": "…", "name": "Authorization", "secret": true, "hint": "…f00d" }
]

A secret header's value is stored encrypted and never shown again — only its hint. A plain header's value is shown as stored.

The fields are ApiWebhookSchema in src/lib/schema/api/api_webhook.schema.ts; the ids are WEBHOOKS in src/lib/const/webhook.const.ts.

GET /api/public/v1/webhooks ​

Paged as every list is — see Pagination. Deleted endpoints are not listed.

ParameterMeaning
limitRows per page, 1–100, default 50.
cursorThe previous page's next_cursor.
event_typeOnly endpoints for this event; repeat it for several.

GET /api/public/v1/webhooks/{id} ​

One endpoint. Another organization's, a deleted one, or an id that is not an endpoint id at all, is a 404.

POST /api/public/v1/webhooks ​

json
{
  "url": "https://hooks.example.com/dialcru",
  "event_type": "call_outcome.submitted",
  "description": "CRM intake",
  "subtypes": ["interested", "callback"],
  "campaign_ids": "all",
  "headers": [
    { "name": "Authorization", "value": "Bearer 3f…", "secret": true }
  ]
}
FieldRequiredDefault
urlYes—
event_typeYes—
methodNoPOST
descriptionNoNone; up to 255 characters
subtypesNo"all"; otherwise a non-empty list of the event's subtypes
campaign_idsNo"all"; otherwise a non-empty list of your campaign ids
headersNonone; up to 20, each name, value, optional secret
body_templateNoThe event's starter template, as the dashboard pre-fills
schedule_templateNonone — every delivery sent at once
content_typeNoapplication/json

201 with the endpoint and its signing secret:

json
{ "data": { "id": "…", "secret": "whsec_…", "secret_hint": "…a1B2", "…": "…" } }

Store secret now. It is in this response and nowhere else; a lost secret is replaced by rotating it (below), not recovered.

The refusals are the dashboard's, as 400 INVALID_INPUT with a path naming the field: a URL that is not https, or that resolves to a private address; a template that names a field the event does not carry, or does not render; a subtype the event does not have; a campaign id that is not one of yours; a campaign_ids list on an event that belongs to no campaign; a reserved header name. Templates lists the rules.

PATCH /api/public/v1/webhooks/{id} ​

Send only what changes; every field of POST is accepted, plus enabled. At least one field is required. 200 with the endpoint as it now stands.

  • enabled: false switches the endpoint off (disabled_reason: "manual"); true switches it back on, whatever switched it off, and clears consecutive_failures. Anything queued while it was off was dropped — see Delivery for resending.
  • headers replaces the whole list. To keep a header, send it back with its id and name and no value: a secret one keeps its stored value without you ever holding it, so this is also how to rename one. Send a value to change it, and omit id to add a header. A secret header cannot be made plain without sending its value. Leave headers out to keep them all.
  • Changing event_type resets subtypes to "all", since subtypes are per event — unless the same request sends subtypes for the new event. The body template is kept, so a template naming fields the new event lacks is refused: send a new body_template with it. campaign_ids carries over, except to an event that belongs to no campaign.
  • description and schedule_template: null clears them.

The settings are saved first, then enabled: a refused change leaves the endpoint switched as it was. The signing secret is never changed by PATCH.

DELETE /api/public/v1/webhooks/{id} ​

Deletes the endpoint: deliveries stop at once, and it cannot be restored. 200:

json
{ "data": { "id": "eeeeeeee-1111-4111-8111-111111111111", "deleted": true } }

A second DELETE of the same endpoint is a 404. To stop deliveries for a while instead, PATCH it with "enabled": false.

POST /api/public/v1/webhooks/{id}/rotate-secret ​

No body. 200 with the endpoint and its new secret, shown once. For 24 hours (WEBHOOKS.ROTATION_GRACE_SEC) deliveries are signed with both the old and the new secret, so you can deploy the new one when it suits you — see Signatures.

POST /api/public/v1/webhooks/{id}/test ​

No body. Renders the endpoint's own template against sample values, sends it, and waits for the answer:

json
{ "data": { "delivered": true } }

delivered is whether your endpoint answered as a success, by the endpoint's own response rules. The test is recorded in the delivery log with is_test: true. A switched-off endpoint is a 409.

GET /api/public/v1/webhooks/{id}/deliveries ​

The endpoint's delivery log, newest first, paged as every list is.

ParameterMeaning
limitRows per page, 1–100, default 50.
cursorThe previous page's next_cursor.
statuspending, sending, delivered, dead or cancelled; repeatable.

Each delivery:

FieldTypeNullableExample
idstring (uuid)No"dddddddd-1111-4111-8111-111111111111"
event_typestringNo"call.completed"
statusstringNo"delivered"
is_testbooleanNofalse
occurred_atstring (ISO)No"2026-11-02T09:00:00.000Z"
attempt_countnumberNo1
next_attempt_atstring (ISO)Yesnull
scheduled_atstring (ISO)Yesnull
schedule_errorstringYesnull
render_errorstringYesnull
created_atstring (ISO)No"2026-11-02T09:00:01.000Z"
last_attemptobjectYessee below

status dead is a delivery we gave up on. last_attempt is null until the first attempt, then:

FieldTypeNullableExample
last_attempt.numbernumberNo1
last_attempt.outcomestringNo"delivered"
last_attempt.status_codenumberYes200
last_attempt.started_atstring (ISO)No"2026-11-02T09:00:02.000Z"
last_attempt.duration_msnumberYes120
last_attempt.errorstringYesnull

outcome is delivered, retry, gone, stop or retired. No body is published — neither what we sent nor what your endpoint answered. Either can carry a phone number, and a page of them would be a bulk export; open the delivery in the dashboard to read them. The log is kept for 90 days.

Unknown fields ​

A body with a field these pages do not name is a 400, not ignored — a misspelt enabeld would otherwise answer 200 having changed nothing.

Rate limits ​

Beside the per-key request budget, creating, changing, rotating and testing an endpoint spend one token each from the organization's webhook editing budget — 60 a minute (WEBHOOKS.WRITE_RATE_LIMIT), shared with the dashboard, since each of them reaches the endpoint's host. Switching an endpoint on or off and deleting one spend none. A spent budget is a 429 TOO_MANY_REQUESTS naming it.