Generated from the engine operation registry and its refusal codes. Publish only while GET /v1/features answers webhooks: true.
Connect your software to updates about calls, minutes and workers. The endpoint must use HTTPS. Its signing secret is shown once.
Operations
Each is POST /v1/ops/{name} with JSON and an owner key (Authorization: Bearer <key>). A helper key cannot manage endpoints.
webhook.create{ "url": "https://your-domain.example/events", "events": ["call.ended"] }(eventsoptional: all when left out). Answer{ "id", "url", "events", "secret", "created_at" }. HTTPS on port 443 only, a domain name, no credentials, query or fragment; it must resolve to public addresses. Thesecretis in this answer only.webhook.list{}. Answer{ "endpoints": [{ "id", "url", "events", "created_at" }] }: active endpoints, never a secret.webhook.update{ "endpoint_id", "events" }(one or more, no repeats). Answer{ "id", "events" }. A newly chosen event is sent from then on, never for the past.webhook.rotate{ "endpoint_id" }. Answer{ "id", "secret" }: the new secret, once; the old one signs nothing from then on.webhook.revoke{ "endpoint_id" }. Answer{ "revoked" }.
Refusals
A refusal is { "error": { "code", "reason", "message", "detail" }, "request_id" }. reason is a fixed code: show your own sentence by it, not by message (English, and it may change). detail is null unless the row names its fields (max: the endpoint limit). When a request has several mistakes, reason is the first one's.
| reason | status | code | operations | when | detail |
|---|---|---|---|---|---|
webhook_url_invalid | 400 | invalid_input | webhook.create | the URL is missing, longer than 2,048 characters, or not allowed (HTTPS on port 443, a domain name, no credentials, query or fragment) | null |
webhook_dns_failed | 400 | invalid_input | webhook.create | the domain could not be resolved | null |
webhook_address_private | 400 | invalid_input | webhook.create | the domain resolves to an address that is not public | null |
webhook_events_invalid | 400 | invalid_input | webhook.create, webhook.update | events is not a list of one or more of the events below with no repeats (required on webhook.update) | null |
webhook_field_unknown | 400 | invalid_input | every webhook operation | a field the operation does not take | null |
webhook_not_found | 404 | not_found | webhook.update, webhook.rotate, webhook.revoke | endpoint_id names no active endpoint of this account (another account's answers the same) | null |
webhook_not_found | 400 | invalid_input | webhook.update, webhook.rotate, webhook.revoke | endpoint_id is missing or longer than 64 characters | null |
webhook_limit | 409 | rejected | webhook.create | the account has reached its endpoint limit | { "max" } |
webhook_registered | 409 | rejected | webhook.create | this URL is already an active endpoint of the account | null |
no_account | 404 | not_found | webhook.create | the account is closed | null |
- reason
webhook_url_invalid- status
- 400
- code
invalid_input- operations
- webhook.create
- when
- the URL is missing, longer than 2,048 characters, or not allowed (HTTPS on port 443, a domain name, no credentials, query or fragment)
- detail
- null
- reason
webhook_dns_failed- status
- 400
- code
invalid_input- operations
- webhook.create
- when
- the domain could not be resolved
- detail
- null
- reason
webhook_address_private- status
- 400
- code
invalid_input- operations
- webhook.create
- when
- the domain resolves to an address that is not public
- detail
- null
- reason
webhook_events_invalid- status
- 400
- code
invalid_input- operations
- webhook.create, webhook.update
- when
eventsis not a list of one or more of the events below with no repeats (required on webhook.update)- detail
- null
- reason
webhook_field_unknown- status
- 400
- code
invalid_input- operations
- every webhook operation
- when
- a field the operation does not take
- detail
- null
- reason
webhook_not_found- status
- 404
- code
not_found- operations
- webhook.update, webhook.rotate, webhook.revoke
- when
endpoint_idnames no active endpoint of this account (another account's answers the same)- detail
- null
- reason
webhook_not_found- status
- 400
- code
invalid_input- operations
- webhook.update, webhook.rotate, webhook.revoke
- when
endpoint_idis missing or longer than 64 characters- detail
- null
- reason
webhook_limit- status
- 409
- code
rejected- operations
- webhook.create
- when
- the account has reached its endpoint limit
- detail
{ "max" }
- reason
webhook_registered- status
- 409
- code
rejected- operations
- webhook.create
- when
- this URL is already an active endpoint of the account
- detail
- null
- reason
no_account- status
- 404
- code
not_found- operations
- webhook.create
- when
- the account is closed
- detail
- null
Events
Each delivery is a JSON POST with the headers x-telfluent-event-id: evt_<id> and x-telfluent-signature: t=<Unix milliseconds>,v1=<hex HMAC-SHA256>. The HMAC input is the timestamp, a dot, then the exact raw body; the key is the endpoint's secret. Compare in constant time, reject an old timestamp, and deduplicate by event id.
Body: { "id": "evt_<id>", "type": "<event>", "data": { ... } }. An endpoint receives only the events it chose.
call.ended:data.call_id.minutes.low:data.remaining_minutes,data.period_start; once a billing period.worker.joined:data.worker_id.
Delivery is at least once and may be out of order. A 2xx answer acknowledges; anything else, a redirect included, is retried with growing waits, a limited number of times over about a day. Each attempt waits at most 10 seconds. No audio, transcript, payment detail or another account's data is ever in an event.