Skip to main content

Webhooks

Outgoing HTTP callbacks when a call starts, ends, or fails to connect. This is the intended way to get transcripts into your own systems — polling the call list is slower and misses the calls that never connected.

Register a webhook​

POST /api/voice/webhooks

{
"name": "CRM sync",
"url": "https://your-server.example/hooks/nextneural",
"events": ["call.started", "call.ended", "call.no_answer"],
"headers": { "X-Tenant": "acme" }
}
FieldRequiredNotes
name✓
url✓Where events are POSTed
events✓At least one — see below
headers—Extra headers sent with every delivery

201 Created:

{
"uuid": "wh1a2b3c-4d5e-6f70-8192-a3b4c5d6e7f8",
"secret": "9f2c4e8a1b3d5f7091a2b3c4d5e6f708"
}
The secret is shown exactly once

It is generated rather than accepted — a caller-chosen secret is routinely a guessable string. Store it now; there is no way to read it back, and no rotate endpoint. To change it, delete the webhook and register a new one.

List webhooks​

GET /api/voice/webhooks

{
"webhooks": [
{
"uuid": "wh1a2b3c-…",
"name": "CRM sync",
"url": "https://your-server.example/hooks/nextneural",
"events": ["call.started", "call.ended"],
"is_active": true,
"last_triggered_at": "2026-09-20T14:33:54Z",
"created_at": "2026-09-02T10:11:12Z"
}
]
}

The secret is not included.

Delete a webhook​

DELETE /api/voice/webhooks/{uuid} → 204

Delivery logs​

GET /api/voice/webhooks/{uuid}/logs

Every attempt is logged whether it succeeded or not — an integration that stopped working is otherwise indistinguishable from one nobody configured.

{
"logs": [
{
"event_type": "call.ended",
"response_status": 200,
"success": true,
"error_message": "",
"created_at": "2026-09-20T14:33:55Z"
},
{
"event_type": "call.ended",
"response_status": 0,
"success": false,
"error_message": "Post \"https://…\": dial tcp: i/o timeout",
"created_at": "2026-09-20T12:04:11Z"
}
]
}

Events​

EventFires when
call.startedA call connects and the pipeline begins
call.endedThe call finishes — carries the transcript
call.no_answerThe provider reported no answer, busy, or failed
call.failedThe call could not be placed

call.no_answer matters for campaigns: those calls never reach the media pipeline at all, so this is the only signal they happened.

Payload​

{
"event": "call.ended",
"timestamp": "2026-09-20T14:33:54Z",
"data": {
"call_uuid": "cal-c1d2e3f4-…",
"from_number": "+915550010000",
"to_number": "+918888888888",
"duration_seconds": 113,
"transcript": [
{ "speaker": "agent", "text": "Hi, is that Priya?", "ts": "2026-09-20T14:32:04Z" },
{ "speaker": "customer", "text": "Yes, speaking.", "ts": "2026-09-20T14:32:07Z" }
]
}
}

call.no_answer carries the shorter form — call_uuid, from_number, to_number, outcome — since there is no conversation to report.

Events fire after the call row is written, so a receiver that immediately calls back sees the finished call rather than one still in progress.

Verifying a delivery​

Every request carries an HMAC-SHA256 of the raw body, keyed with your secret:

X-Webhook-Signature: sha256=<hex digest>
User-Agent: voice-bot-ai-webhooks/1
Content-Type: application/json
import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)

Compute it over the raw bytes, before any JSON parsing — re-serializing changes the bytes and the digest with them.

Delivery behaviour​

  • One attempt per event. There is no retry and no backoff. A receiver that is down misses that event permanently; reconcile from the call list.
  • Any status ≥ 300 counts as failure, recorded in the logs with the status.
  • Deliveries are sequential across your endpoints, not parallel.
  • Only the first 4 KB of your response is read, then discarded.

Return 2xx quickly and do the work asynchronously. A slow receiver delays delivery to your other endpoints.