Calls
Place outbound calls and read their results.
A call is a single phone interaction placed or answered by one of your agents. Place calls with POST /v1/calls, then read status, transcript, and extracted data back.
The call object (list shape):
{
"id": "call_…",
"contactId": "con_…",
"campaignId": null,
"channel": "voice",
"direction": "outbound",
"status": "completed",
"outcome": "call_initiated",
"startedAt": "2026-08-20T12:00:00Z",
"endedAt": "2026-08-20T12:01:36Z",
"durationSeconds": 96
}
durationSeconds is null until the call has both a start and end time. Internal fields (agent, room, account) are never returned.
Place a call
POST /v1/calls · scope calls:write · rate limited
curl -X POST https://api.oyehello.ai/v1/calls \
-H "Authorization: Bearer ohk_live_…" \
-H "Content-Type: application/json" \
-d '{
"agentId": "agt_…",
"toNumber": "+919812345678",
"externalRef": "your-record-id",
"variables": { "name": "Nalin", "amount": "4999" }
}'
Body
| Field | Type | Required | Notes |
|---|---|---|---|
agentId |
string | yes | The agent that runs the call. |
toNumber |
string | yes | Destination - a valid Indian +91 subscriber number (8–20 chars). |
fromNumber |
string | no | Caller ID. Must be an active number you own. Defaults to your first active number. |
externalRef |
string | no | Your own stable id (1–120 chars). Reuses the same contact across calls and comes back on webhooks. Omit and a synthetic contact keyed to the number is used. |
variables |
object | no | String values substituted into the agent’s {variables} at call time. |
Response - 200
This endpoint returns a bare object (no data wrapper):
{ "placed": true, "interactionId": "call_…", "providerCallId": "prov_…" }
Use interactionId to fetch the call later. The call runs the agent’s full compliance pipeline exactly as a campaign dial does.
Gates & errors
The same production gates that guard campaign dialing apply here:
| Status | Error | Cause |
|---|---|---|
404 |
agent not found |
Unknown agentId. |
403 |
workspace suspended |
The workspace is suspended. |
403 |
workspace must be verified (KYB) before placing calls |
Complete KYB first. |
400 |
toNumber must be a valid Indian (+91) subscriber number |
Bad destination. |
400 |
buy a phone number before placing calls |
No owned number to dial from. |
402 |
insufficient wallet balance - top up to place a call |
Wallet can’t cover the call (in-flight calls counted). |
422 |
call blocked by compliance: <reason> |
DND, opt-out, quiet-hours, or frequency cap. |
429 |
concurrent-call limit reached … / platform is at capacity … |
Too many calls in flight - retry shortly. |
503 |
India-resident voice providers are not configured … |
Workspace requires India-resident providers that aren’t set up. |
502 |
call could not be placed |
Provider/dispatch failure. |
List calls
GET /v1/calls · scope calls:read
Query
| Param | Type | Notes |
|---|---|---|
limit |
int | 1–200, default 50. |
status |
string | Optional - filter by call status. |
Response - 200
{ "data": [ { "id": "call_…", "status": "completed", … } ] }
Returns a single page (there’s no cursor paging); raise limit up to 200 for more per request.
Get a call
GET /v1/calls/:id · scope calls:read
Returns the full call, including the transcript, extracted fields, and a recording URL:
{
"data": {
"id": "call_…",
"status": "completed",
"outcome": "interested",
"durationSeconds": 96,
"transcript": [
{ "role": "agent", "text": "Hi, this is Riya from OyeHello…" },
{ "role": "customer", "text": "Yes, go ahead." }
],
"extracted": { "callback": "Tuesday" },
"recordingUrl": "https://…signed…"
}
}
transcript- the diarized conversation ([]if none).extracted- the structured fields the agent captured ({}if none).recordingUrl- a signed playback URL if a recording exists, elsenull.
Unknown id → 404 { "error": "call not found" }.
Next: Agents →