Skip to content

Developers

Webhooks

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"] } (events optional: 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. The secret is 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
    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
    events is 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_id names 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_id is 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.