Calls
A call placed through the API runs an AI voice agent against one phone number. The call is created in the queued state and moves through the statuses below. To place many calls to a list of people, use Campaigns.
Call statuses
Section titled “Call statuses”| Status | Meaning |
|---|---|
queued |
Accepted, not dialed yet. |
dialing |
The voice service is dialing. |
in_progress |
The person answered. |
completed |
The call was answered and has ended. |
no_answer |
Nobody answered. |
busy |
The line was busy. |
failed |
The call could not be placed. |
refused |
The call was refused, for example because AI voice minutes were not available. |
queued, dialing and in_progress count toward your concurrent live call limit.
The call object
Section titled “The call object”{ "id": "call_5b7e2d90c1a44f3e8d21a0b7", "agent_id": "agent_8f3a1c", "agent_version_id": "ver_21d4e0", "to_number": "+919876543210", "lead_id": "lead_77c1e9", "status": "completed", "error": null, "metadata": { "order_id": "A-1042" }, "duration_seconds": 94, "created_at": "2026-11-04T09:30:00", "answered_at": "2026-11-04T09:30:12", "ended_at": "2026-11-04T09:31:34"}| Field | Description |
|---|---|
id |
The call id. |
agent_id, agent_version_id |
The agent and the published version that placed the call. |
to_number |
The number dialed, in E.164. |
lead_id |
The Sahay lead for this number. Sahay finds or creates one when it places the call. null if the call failed before that step. |
status |
One of the statuses above. |
error |
A short reason when the call failed or was refused, otherwise null. |
metadata |
The object you sent when you placed the call, or null. |
duration_seconds |
Length of the call in seconds, or null until known. |
created_at, answered_at, ended_at |
Timestamps. answered_at and ended_at are null until they happen. |
GET /calls/{call_id} adds transcript, summary, outcome and has_recording to this object.
Place a call
Section titled “Place a call”POST /calls
Places one AI call. It answers 202 with the new call in the queued state.
| Field | Type | Required | Description |
|---|---|---|---|
agent_id |
string | yes | A published, active, custom agent in your organization. |
to_number |
string | yes | The number to call, in E.164: a plus sign, a non-zero country digit, then 8 to 15 digits in all. |
metadata |
object | no | Any JSON object, up to 4096 bytes when encoded as UTF-8 JSON. It is returned on the call and in the call.completed webhook. |
No other fields are accepted.
| Header | Description |
|---|---|
Idempotency-Key |
Optional, 1 to 128 characters. Send it so a retry never dials twice. |
Idempotency
Section titled “Idempotency”If you send an Idempotency-Key that your organization has used before, the API does not dial again. It answers 200 (instead of 202) with the original call. Use a fresh key for each call you intend to place, for example one derived from your own order id. Use the same key when you retry the same call after a timeout or a 5xx.
Keys are scoped to your organization.
curl -X POST "$BASE_URL/calls" \ -H "Authorization: Bearer $SAHAY_API_KEY" \ -H "Idempotency-Key: 6f1d2c9e-order-A-1042" \ -H "Content-Type: application/json" \ -d '{"agent_id": "agent_8f3a1c", "to_number": "+919876543210", "metadata": {"order_id": "A-1042"}}'Response 202:
{ "id": "call_5b7e2d90c1a44f3e8d21a0b7", "agent_id": "agent_8f3a1c", "agent_version_id": "ver_21d4e0", "to_number": "+919876543210", "lead_id": null, "status": "queued", "error": null, "metadata": { "order_id": "A-1042" }, "duration_seconds": null, "created_at": "2026-11-04T09:30:00", "answered_at": null, "ended_at": null}The response can already show dialing and a lead_id by the time you read it. Read the call later, or use the call.completed webhook, for the result.
Errors:
400 invalid_idempotency_key: the key must be 1 to 128 characters.402 insufficient_voice_minutes: the plan has no AI voice minutes, or none are available right now.404 agent_not_found409 agent_not_published: the agent must be active, custom and published.409 no_caller_id,409 agent_number_paused: the agent has no usable number to call from.422 invalid_request: for example ato_numberthat is not E.164, an unknown field, ormetadataover 4096 bytes.422 number_not_callable: the number is blocklisted, opted out, or a Sahay voice number.429 live_call_limit: your organization already has its maximum number of calls in progress.502 executor_unreachable,502 executor_refused: the voice service did not answer or refused the call. Afterexecutor_unreachablethe call may have rung.503 lines_busy(withRetry-After),503 voice_not_configured,503 entitlement_check_failed
When a call fails after it was created, the call is kept with the status failed or refused and a reason in error.
List calls
Section titled “List calls”GET /calls
Lists calls, newest first.
| Parameter | In | Description |
|---|---|---|
limit |
query | Page size, 1 to 100. Default 25. |
cursor |
query | The next_cursor of the previous page. |
status |
query | One of queued, dialing, in_progress, completed, failed, no_answer, busy or refused. |
agent_id |
query | Only calls of this agent. |
since |
query | ISO 8601 timestamp. Only calls created at or after it. |
curl "$BASE_URL/calls?status=completed&limit=25" \ -H "Authorization: Bearer $SAHAY_API_KEY"Response 200:
{ "data": [ { "id": "call_5b7e2d90c1a44f3e8d21a0b7", "agent_id": "agent_8f3a1c", "agent_version_id": "ver_21d4e0", "to_number": "+919876543210", "lead_id": "lead_77c1e9", "status": "completed", "error": null, "metadata": { "order_id": "A-1042" }, "duration_seconds": 94, "created_at": "2026-11-04T09:30:00", "answered_at": "2026-11-04T09:30:12", "ended_at": "2026-11-04T09:31:34" } ], "next_cursor": "MjAyNi0xMS0wNFQwOTozMDowMHxjYWxsXzVi"}next_cursor is null on the last page. List items do not include the transcript.
Errors: 400 invalid_status, 400 invalid_limit (above 100), 400 invalid_cursor, 422 invalid_request (for example limit=0 or a since that is not a timestamp).
Get a call
Section titled “Get a call”GET /calls/{call_id}
Returns a call with its status, transcript, summary, outcome and duration.
| Parameter | In | Description |
|---|---|---|
call_id |
path | The call id. |
curl "$BASE_URL/calls/call_5b7e2d90c1a44f3e8d21a0b7" \ -H "Authorization: Bearer $SAHAY_API_KEY"Response 200:
{ "id": "call_5b7e2d90c1a44f3e8d21a0b7", "agent_id": "agent_8f3a1c", "agent_version_id": "ver_21d4e0", "to_number": "+919876543210", "lead_id": "lead_77c1e9", "status": "completed", "error": null, "metadata": { "order_id": "A-1042" }, "duration_seconds": 94, "created_at": "2026-11-04T09:30:00", "answered_at": "2026-11-04T09:30:12", "ended_at": "2026-11-04T09:31:34", "transcript": "Agent: Hello, this is the front desk at Acme.\nCaller: Hi, are you open on Saturday?", "summary": "Caller asked about Saturday hours and booked a visit for 11am.", "outcome": "appointment_booked", "has_recording": true}transcript, summary and outcome stay null until the call has been processed, which happens after it ends. transcript is plain text. has_recording is false until a recording exists.
Errors: 404 call_not_found.
Get a recording
Section titled “Get a recording”GET /calls/{call_id}/recording
Returns a short-lived URL for the call’s recording. expires_in is in seconds. Fetch the audio from url before it expires, and request a new URL when you need it again.
curl "$BASE_URL/calls/call_5b7e2d90c1a44f3e8d21a0b7/recording" \ -H "Authorization: Bearer $SAHAY_API_KEY"Response 200:
{ "url": "https://<storage-host>/<recording-path>?<signature>", "expires_in": 300}Errors: 403 recording_private, 404 call_not_found, 404 recording_not_found (the call has no recording yet).