HeyKoala Voice APIAPI

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

EventWhenWhat it is for
call.startedDialling began.Show "calling…" in your UI.
call.connectedThe callee answered.The conversation is happening.
call.endedThe call finished normally.Duration is final. Summary is not ready yet.
call.failedIt never connected — busy, no answer, dispatch error.outcome_code says which.
call.recording_availableRecording egress finished.Fetch a link from GET /call_logs/{id}/media/.
call.analysis_completedPost-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

AttemptDelay
1immediate
230 seconds
32 minutes
48 minutes
532 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.

On this page