Skip to content

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.

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.

{
"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.

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.

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.

Terminal window
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_found
  • 409 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 a to_number that is not E.164, an unknown field, or metadata over 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. After executor_unreachable the call may have rung.
  • 503 lines_busy (with Retry-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.

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.
Terminal window
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 /calls/{call_id}

Returns a call with its status, transcript, summary, outcome and duration.

Parameter In Description
call_id path The call id.
Terminal window
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 /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.

Terminal window
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).