HeyKoala Voice APIAPI

Placing a call

What dispatch needs, what it returns, and why every call needs a contact.

curl -X POST https://dvoice.heykoala.ai/api/v1/ext/calls/dispatch/ \
  -H "Authorization: Bearer hk_api_..." \
  -H "Content-Type: application/json" \
  -d '{
        "to_number": "+919876543210",
        "variables": { "lead_id": "EZ-4471", "name": "Asha" },
        "event_webhook_url": "https://your-app.example/hooks/heykoala"
      }'
{
  "status": true,
  "message": "Created.",
  "data": {
    "call_id": "ddd20991-cf9a-44fb-9953-5a16b405f5a0",
    "status": "pending",
    "provider": "twilio",
    "dispatched": true,
    "message": ""
  },
  "error": null
}

Requires calls: write. The response returns as soon as the call is handed to the telephony provider — the conversation has not happened yet. What happened arrives on your webhook.

Every call must have a contact

to_number must already exist as a contact in your organization, or you must pass contact_id. If neither resolves, the call is refused:

{
  "status": false,
  "message": "No contact found for +919999900000. Every call must be attached to a contact, ...",
  "data": null,
  "error": { "errors": { "to_number": ["No contact found for +919999900000. ..."] } }
}

This is deliberate. Every webhook carries a contact block, and that block is how you attach the conversation to the right lead in your own system. A call with no contact produces events with contact: null — technically valid, and useless to the receiving CRM. Refusing at dispatch is better than delivering an unattributable outcome an hour later.

So the flow is always create the contact, then call it:

curl -X POST .../contacts/ -d '{
  "phone": "+919876543210",
  "name": "Asha Menon",
  "custom_fields": { "lead_id": "EZ-4471" }
}'

Creating a contact is idempotent in practice — if the number already exists, you get a validation error naming the conflict, and you can look it up with GET /contacts/search/?q=+919876543210.

Choosing what runs the call

Every field below is optional; each falls back to a sensible default.

FieldDefault
agent_idYour first active, verified agent.
call_flow_idNone — the agent runs on its base prompt alone.
from_numberYour first active number. Must be one you own.
knowledge_base_idNone — no retrieval during the call.

from_number is validated against numbers your organization actually owns. You cannot set an arbitrary caller id.

Passing your own identifiers

variables does double duty: the values are available to the agent's prompt, and every webhook for the call echoes them back verbatim. Put your own lead_id, lead_source, ticket number — whatever you need to match the outcome to your record — and you never have to store a mapping from our call_id to yours.

Reading the result

dispatched: true means the provider accepted the call. dispatched: false means the call log exists but the call never went out — message says why, and the call log carries a terminal outcome. Either way you get a call_id.

Then either:

  • Wait for webhooks (recommended) — set event_webhook_url and receive each lifecycle event, ending with call.analysis_completed carrying the summary, sentiment and lead score. See Webhooks.
  • Poll GET /call_logs/{id}/ if you cannot receive HTTP. Analysis can take minutes after the call ends, so poll gently.

Recording and transcript links are not in the call log — ask for signed ones with GET /call_logs/{id}/media/ when you need them. They expire, so fetch on demand rather than storing.

Costs

A dispatched call consumes credits, billed per minute and rounded up. Running out mid-campaign pauses the campaign; running out before a dispatch answers 429.

On this page