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:
- 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. - REST-hooks subscription API —
/api/v1/hooks,X-API-Keyonly, 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.
| Method | Path | Purpose |
|---|---|---|
GET | /api/orgs/{orgId}/webhooks | List endpoints |
POST | /api/orgs/{orgId}/webhooks | Create an endpoint (signing secret returned once) |
PATCH | /api/orgs/{orgId}/webhooks/{id} | Change an endpoint's events and call.ended extras |
DELETE | /api/orgs/{orgId}/webhooks/{id} | Disable an endpoint |
GET | /api/orgs/{orgId}/webhooks/{id}/deliveries | Delivery history (paginated) |
Creating or editing requires an owner, admin, or developer role, deleting an owner or admin; 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
}'
| Field | Type | Notes |
|---|---|---|
url | string | Required. Your HTTPS endpoint. Plain http://, loopback, and private/metadata IPs are rejected (SSRF guard). |
events | string[] | Event names to receive, or ["*"] for all (the default if omitted). An empty list, or a name that is not an event, is refused with 400 and the valid names; nothing is created. See events |
agentId | UUID | Optional. Scope to one agent; omit/null for org-wide (matches every call) |
includeRecording | boolean | Add the recording (signed URL) to call.ended payloads |
includeTranscript | boolean | Add 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 returnedendpointobject uses snake_case (include_recording,agent_id,created_at). Read responses by snake_case key. Thesigning_secretis 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.
Edit an endpoint
PATCH /api/orgs/{orgId}/webhooks/{id}
curl -X PATCH https://api.telenow.ai/api/orgs/{orgId}/webhooks/{id} \
-H "x-api-key: vai_live_…" \
-H "Content-Type: application/json" \
-d '{ "events": ["call.ended", "recording.ready", "transcript.ready"] }'
| Field | Type | Notes |
|---|---|---|
events | string[] | The whole new list: event names, or ["*"] for all. At least one: an empty list is refused, as on create |
includeRecording | boolean | Add the recording (signed URL) to call.ended payloads |
includeTranscript | boolean | Add the transcript array to call.ended payloads |
A field you leave out keeps its value. The answer is the updated endpoint, as on create, without a secret.
- Your signing secret stays the same, so your receiver needs no change.
urlandagentIdcan't be edited, and a body naming them or the secret is refused with422. To change them, create a new endpoint: it gets its own signing secret.- Deliveries already queued still go out, even for an event you just removed. The new list decides what is queued from now on.
- While your call recordings are sent to your webhook and not kept in Telenow, the last endpoint for all agents that receives
recording.audiocan't drop it: the edit is refused with409.
Scoping & matching
An endpoint receives an event when both are true:
- its
eventscontains"*"or the exact event name, and - its
agentIdisnull(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.
If your organization has sub-accounts, its org-wide endpoints also receive every event your sub-accounts raise, each carrying subAccountId. Agent-scoped endpoints do not.
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.
While your call recordings are sent to your webhook and not kept in Telenow, the last endpoint for all agents that receives recording.audio can't be deleted: the delete is refused with 409 and nothing changes. Add recording.audio to another endpoint for all agents first, or keep recordings in Telenow again.
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.
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/hooks | List subscriptions (?source= to filter by platform) |
POST | /api/v1/hooks | Subscribe 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"
}'
| Field | Type | Notes |
|---|---|---|
target_url | string | Required. Where to POST events (aliases: targetUrl, url). Same HTTPS/SSRF rules as above. |
event | string | A single event name… |
events | string[] | …or several at once. If neither is given, defaults to ["*"]. Unknown names are rejected with the valid list. |
agentId | UUID | Optional. Scope to one agent (alias: agent_id). |
includeRecording | boolean | Enrich call.ended with the recording (alias: include_recording). |
includeTranscript | boolean | Enrich call.ended with the transcript (alias: include_transcript). |
source | string | Free-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 — 23, 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.completed · call.machine_detected · call.dtmf ·
transcript.ready · tool.invoked · recording.ready · recording.audio ·
recording.failed · call.analyzed
recording.audio reaches only an endpoint that names it: * doesn't include it. It
carries the audio itself while your recordings are
sent to you instead of kept.
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 · billing.spend_state_changed
This list was wrong until 2026-09-12 and under-reported what the API accepts.
call.machine_detected,call.dtmfand the threewhatsapp.*events had been accepted for some time; the eightbilling.*events were being delivered but refused by name — reachable only through an endpoint subscribed to["*"]. If you tried to subscribe to one and got a400, it works now. Payloads for all of them are in the webhook events reference.
call.completedis subscribable since 2026-10-17 — one event per finished call, unanswered ones included. The campaign docs already pointed to it whilePOST /hooksstill refused the name.
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 23 names above. For recording.audio the sample is its
metadata part. 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, "…": "…" } ] }
Related
- Webhook events reference — every event and payload.
- Receive & verify webhooks — building a secure receiver.
- Automation platforms — Zapier / n8n / Make / viaSocket setup.