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.
| Field | Default |
|---|---|
agent_id | Your first active, verified agent. |
call_flow_id | None — the agent runs on its base prompt alone. |
from_number | Your first active number. Must be one you own. |
knowledge_base_id | None — 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_urland receive each lifecycle event, ending withcall.analysis_completedcarrying 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.