Webhook management

Webhook management API

Register and manage the endpoints Telenow delivers events to. For the events themselves, payloads, and signature verification see Webhook events reference and the integration guide.

There are two ways to manage webhook endpoints; they write to the same delivery system, so events behave identically once registered:

  1. Dashboard management API/api/orgs/{orgId}/webhooks, JWT or API key, returns the {success,data} envelope. This is what the dashboard Webhooks page uses, and where the signing secret is issued.
  2. REST-hooks subscription API/api/v1/hooks, X-API-Key only, flat JSON. Built for automation platforms (Zapier, n8n, Make, viaSocket) that subscribe/unsubscribe programmatically. See Automation platforms.

Use the dashboard API when you control the receiver and want signed, verifiable deliveries; use the REST-hooks API when an automation platform is managing the subscription for you.


1. Dashboard management API

Organization-scoped. Authenticate with an API key or a user JWT + X-Org-Id.

MethodPathPurpose
GET/api/orgs/{orgId}/webhooksList endpoints
POST/api/orgs/{orgId}/webhooksCreate an endpoint (signing secret returned once)
DELETE/api/orgs/{orgId}/webhooks/{id}Disable an endpoint
GET/api/orgs/{orgId}/webhooks/{id}/deliveriesDelivery history (paginated)

Creating/deleting requires an owner, admin, or developer role; listing is open to any member.

Create an endpoint

POST /api/orgs/{orgId}/webhooks
curl -X POST https://api.telenow.ai/api/orgs/{orgId}/webhooks \
  -H "x-api-key: vai_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/telenow/webhooks",
    "events": ["call.ended", "recording.ready"],
    "includeRecording": true,
    "includeTranscript": true
  }'
FieldTypeNotes
urlstringRequired. Your HTTPS endpoint. Plain http://, loopback, and private/metadata IPs are rejected (SSRF guard).
eventsstring[]Event names to receive, or ["*"] for all (the default if omitted). See events
agentIdUUIDOptional. Scope to one agent; omit/null for org-wide (matches every call)
includeRecordingbooleanAdd the recording (signed URL) to call.ended payloads
includeTranscriptbooleanAdd the transcript array to call.ended payloads

The response wraps the new endpoint and surfaces the signing_secret alongside it — shown once. Store it; you'll need it to verify signatures.

{
  "success": true,
  "data": {
    "endpoint": {
      "id": "…",
      "org_id": "…",
      "agent_id": null,
      "url": "https://example.com/telenow/webhooks",
      "events": ["call.ended", "recording.ready"],
      "include_recording": true,
      "include_transcript": true,
      "source": "dashboard",
      "last_ok_at": null,
      "last_error": null,
      "disabled_at": null,
      "created_by": "…",
      "created_at": "2026-06-13T10:00:00Z",
      "updated_at": "2026-06-13T10:00:00Z"
    },
    "signing_secret": "wh_secret_…"
  }
}

Field casing: the request body uses camelCase (includeRecording, agentId), but the returned endpoint object uses snake_case (include_recording, agent_id, created_at). Read responses by snake_case key. The signing_secret is returned only on create — it's omitted from every list/read response.

The signing_secret is not returned by GET (list) — list responses omit it so no member can read another team's HMAC key. If you lose it, delete the endpoint and create a new one.

Scoping & matching

An endpoint receives an event when both are true:

  • its events contains "*" or the exact event name, and
  • its agentId is null (org-wide) or equals the call's agent.

So an org-wide endpoint subscribed to "*" receives everything; an agent-scoped endpoint only receives its agent's events.

Deliveries & retries

GET …/{id}/deliveries returns recent delivery attempts (status, response status/body, timestamps) for debugging — paginated with limit (default 50, max 200) and offset. Telenow retries failed deliveries with exponential backoff and disables an endpoint after repeated permanent failures — see Receive & verify webhooks.

Disabling

DELETE …/{id} soft-disables the endpoint (delivery history is kept for auditing). Recreate to re-enable with a fresh secret.


2. REST-hooks subscription API

For automation platforms. X-API-Key only (no JWT), flat JSON responses, and the org is taken from the key — a key for org A can't subscribe hooks for org B. The subscribing key needs an owner, admin, or developer role.

MethodPathPurpose
GET/api/v1/hooksList subscriptions (?source= to filter by platform)
POST/api/v1/hooksSubscribe a target_url to one or more events
DELETE/api/v1/hooks/{id}Unsubscribe (idempotent — already-gone returns 200)

Subscribe

POST /api/v1/hooks
curl -X POST https://api.telenow.ai/api/v1/hooks \
  -H "X-API-Key: vai_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "target_url": "https://hooks.zapier.com/...",
    "event": "call.ended",
    "source": "zapier"
  }'
FieldTypeNotes
target_urlstringRequired. Where to POST events (aliases: targetUrl, url). Same HTTPS/SSRF rules as above.
eventstringA single event name…
eventsstring[]…or several at once. If neither is given, defaults to ["*"]. Unknown names are rejected with the valid list.
agentIdUUIDOptional. Scope to one agent (alias: agent_id).
includeRecordingbooleanEnrich call.ended with the recording (alias: include_recording).
includeTranscriptbooleanEnrich call.ended with the transcript (alias: include_transcript).
sourcestringFree-form platform label, normalized to [a-z0-9_-] (e.g. zapier, n8n, make). Defaults to api. Lets each platform list/delete only its own hooks.

Valid event names — 19, plus *. Anything else is rejected with 400 and the full valid list in the error body, so the API is always the authority if this page drifts.

Session-scoped (carry sessionId, agentId, identifier):

call.started · call.ended · call.machine_detected · call.dtmf · transcript.ready · tool.invoked · recording.ready · call.analyzed

Org-scoped (no sessionId, no agentId — an endpoint with agentId set never receives these):

whatsapp.message.received · whatsapp.message.status · whatsapp.account.health · billing.invoice_issued · billing.payment_overdue · billing.account_suspended · billing.number_renewal_due · billing.number_renewed · billing.number_renewal_failed · billing.number_release_pending · billing.number_released_nonpayment

This list was wrong until 2026-09-12 and under-reported what the API accepts. call.machine_detected, call.dtmf and the three whatsapp.* events had been accepted for some time; the eight billing.* events were being delivered but refused by name — reachable only through an endpoint subscribed to ["*"]. If you tried to subscribe to one and got a 400, it works now. Payloads for all of them are in the webhook events reference.

Response (201 Created, flat):

{
  "id": "…",
  "target_url": "https://hooks.zapier.com/...",
  "events": ["call.ended"],
  "agent_id": null,
  "include_recording": false,
  "include_transcript": false,
  "source": "zapier",
  "created_at": "2026-06-13T10:00:00Z"
}

The REST-hooks API does not return a signing secret. Platform deliveries still carry X-VoiceAI-Signature, but the platform manages verification (or skips it for trusted webhook URLs like Zapier's). If you need to verify signatures yourself, register via the dashboard API instead.

List

GET /api/v1/hooks?source=zapier
{ "hooks": [ { "id": "…", "target_url": "…", "events": ["call.ended"], "source": "zapier", "created_at": "…" } ], "total": 1 }

Unsubscribe

DELETE /api/v1/hooks/{id}
{ "id": "…", "deleted": true }

Idempotent: an already-removed hook returns { "deleted": false } with 200. A receiver that responds 410 Gone to a delivery is auto-unsubscribed (the Zapier/REST-hooks convention) — handy when a Zap is turned off without a clean unsubscribe.

Sample payloads

GET /api/v1/events/sample?type=call.ended

Returns the org's most recent real payload of that type (or an obviously-fake canned one if there's no history yet) — used by automation platforms to populate field pickers.

type accepts any of the 19 names above. Field pickers built from a canned sample (is_real: false) should note that money fields — totalBilled, rentUsd, shortfallUsd — are decimal strings, and that billing.number_released_nonpayment has two payload shapes discriminated by reason.

{ "event_type": "call.ended", "is_real": true, "samples": [ { "event": "call.ended", "sessionId": "…", "durationSecs": 142, "…": "…" } ] }