Webhooks
The six call events, their payloads, retry behaviour and how to debug a delivery.
Calls are asynchronous. You start one and we tell you what happened, by POSTing JSON to a URL you own.
Subscribing
Per call, at dispatch:
{
"to_number": "+919876543210",
"event_webhook_url": "https://your-app.example/hooks/heykoala",
"event_webhook_headers": { "Authorization": "Bearer your-own-secret" },
"event_webhook_events": ["call.ended", "call.analysis_completed"]
}event_webhook_events is optional — omit it and you receive all six.
event_webhook_headers are sent with every delivery, which is how you
authenticate us to your endpoint.
The six events
| Event | When | What it is for |
|---|---|---|
call.started | Dialling began. | Show "calling…" in your UI. |
call.connected | The callee answered. | The conversation is happening. |
call.ended | The call finished normally. | Duration is final. Summary is not ready yet. |
call.failed | It never connected — busy, no answer, dispatch error. | outcome_code says which. |
call.recording_available | Recording egress finished. | Fetch a link from GET /call_logs/{id}/media/. |
call.analysis_completed | Post-call analysis finished. | Summary, sentiment, lead score, extracted answers. |
The one to build on is call.analysis_completed. call.ended fires as soon
as the audio stops; the summary and transcript are not ready then. Analysis can
arrive minutes later.
A call ends with either call.ended or call.failed, never both.
Payload
Every event has the same top-level shape:
{
"id": "d1d75369-73e0-4bfa-9c3f-f4d134b590cc",
"event": "call.ended",
"occurred_at": "2026-08-10T08:30:22.187666+00:00",
"call": {
"call_id": "e21babb7-c056-40aa-8d07-73f5fbc607d4",
"direction": "outbound",
"status": "completed",
"outcome": null,
"outcome_code": null,
"disposition": "interested",
"from_number": "+911234567890",
"to_number": "+919876543210",
"started_at": "2026-08-10T08:30:19.991354+00:00",
"answered_at": null,
"ended_at": "2026-08-10T08:30:19.991357+00:00",
"duration_seconds": 97,
"attempt_number": 1,
"campaign_id": "bf0db40a-001d-4a5d-92fa-b8f7c24fba0e",
"agent": { "id": "34981331-...", "name": "Noora" },
"contact": {
"id": "a742a438-...",
"name": "Asha Menon",
"phone": "+919876543210",
"email": "asha@example.com",
"custom_fields": { "lead_id": "EZ-4471", "city": "Bangalore" }
},
"variables": { "lead_id": "EZ-4471", "name": "Asha" }
},
"data": { "event": "room_finished", "room": { "sid": "RM_abc123" } }
}contact and variables are how you find the right record on your side.
data carries the raw upstream event so nothing is lost — its shape varies and
is not a stable contract. Build against call.
call.analysis_completed adds two blocks:
{
"analysis": {
"summary": "The caller asked about pricing and booked a follow-up.",
"short_summary": "Interested — follow-up booked.",
"sentiment": "positive",
"lead_score": 82.0,
"disposition": "interested",
"extracted_responses": [
{ "question": "What is your budget range?", "variableName": "budget",
"answer": "Around 90 lakhs", "weight": 8 }
]
},
"media": {
"recording_url": "https://…?X-Amz-Signature=…",
"transcript_url": "https://…?X-Amz-Signature=…",
"expires_in": 3600
}
}The media links are signed and short-lived. Download the file if you need to
keep it; do not store the URL.
Per-event schemas are in the API reference.
Responding
Return any 2xx. We do not read the body.
Respond fast and do the work afterwards — queue the payload and return
200. A slow endpoint gets retried, which means duplicates.
Retries
| Attempt | Delay |
|---|---|
| 1 | immediate |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 8 minutes |
| 5 | 32 minutes |
Retried on network errors, timeouts, 408, 429, and any 5xx.
Not retried on other 4xx — a 404 or 401 means your endpoint is
misconfigured, and retrying cannot fix it.
Make your handler idempotent
Retries and replays both re-send the same payload. id is unique per delivery
and call_id identifies the call — key on one of them and ignore what you have
already processed. Assume you will occasionally get a duplicate; it is far
cheaper than us dropping an event.
Debugging a delivery
Every attempt is logged and readable:
# What did we send for this call, and did it land?
curl "https://dvoice.heykoala.ai/api/v1/ext/webhooks/?call_id=e21babb7-..." \
-H "Authorization: Bearer hk_api_..."
# The exact bytes we POSTed, plus your response.
curl "https://dvoice.heykoala.ai/api/v1/ext/webhooks/{delivery_id}/" \
-H "Authorization: Bearer hk_api_..."The detail view includes payload (what we sent), response_status and
response_body (what you returned), and attempts. That answers most "we
never got it" questions in one request.
Once your endpoint is fixed, replay:
curl -X POST https://dvoice.heykoala.ai/api/v1/ext/webhooks/replay/ \
-H "Authorization: Bearer hk_api_..." \
-H "Content-Type: application/json" \
-d '{"id": "c030a0c7-..."}'Replay re-sends the original payload unchanged, with a fresh retry budget.
Requirements
- HTTPS, publicly reachable.
http://and non-HTTP schemes are refused at dispatch. - Reachable from our servers — a localhost tunnel is fine for development, but it has to be up when the call ends, not when you started it.