> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lehar.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Calls

> Place a single outbound phone call from your backend with an API key.

A **call** is one outbound phone call, fired server-to-server. It is the primitive to reach for when your own system decides who to ring and when — a CRM trigger, a queue worker, a workflow step — and you don't want to model it as a campaign.

Each call is a durable record that owns its own retries, calling window and concurrency slot. Every dial it makes creates a [session](/concepts/sessions), so one call can have several sessions attached to it.

<Note>
  Three ways to start a voice conversation, and they are not interchangeable:

  * **`POST /calls`** — one outbound phone call, **API key only**, no human in the loop. This page.
  * **`POST /sessions`** — a web (browser) call for a signed-in user; requires a **bearer login** and returns a `participant_token`.
  * **[Campaigns](/concepts/campaigns)** — bulk outreach over a recipient list, with scheduling and per-recipient state.
</Note>

## Endpoints

| Method | Path                      | Auth / Scope                            |
| ------ | ------------------------- | --------------------------------------- |
| POST   | `/calls`                  | **`X-API-KEY` only** + `sessions:write` |
| GET    | `/calls`                  | `sessions:read`                         |
| GET    | `/calls/{call_id}`        | `sessions:read`                         |
| POST   | `/calls/{call_id}/cancel` | `sessions:write`                        |

<Warning>
  `POST /calls` rejects bearer logins **and** agent-session tokens even though both can carry `sessions:write`. An agent-session token lives inside every running voice worker for the duration of a call, so accepting one would let the credential minted for a call originate the next one. Use a workspace API key.
</Warning>

## Place a call

```bash theme={null}
curl -X POST "$LEHAR_BASE_URL/calls" \
  -H "X-API-KEY: $LEHAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agent_sales_hi",
    "to_number": "+919876543210",
    "callee_name": "Jane",
    "custom_variables": { "company": "Acme" },
    "idempotency_key": "crm-lead-8821",
    "call_config": {
      "max_call_length": 600,
      "call_retry_config": { "retry_count": 2, "retry_not_picked": 30 },
      "call_time": { "call_start_time": "10:00", "call_end_time": "19:00", "timezone": "Asia/Kolkata" }
    }
  }'
```

The agent must be **published** and `channel=voice`.

### Request fields

The body is a strict allowlist — an unrecognised field is a `400` naming it, so a setting can never appear to take effect when it didn't.

| Field                  | Required | Notes                                                                                           |
| ---------------------- | -------- | ----------------------------------------------------------------------------------------------- |
| `agent_id`             | Yes      | A published voice agent.                                                                        |
| `to_number`            | Yes      | E.164, e.g. `+919876543210`.                                                                    |
| `callee_name`          | No       | Up to 128 characters.                                                                           |
| `from_phone_number_id` | No       | Caller ID to dial from; defaults to the workspace's number. See [Channels](/concepts/channels). |
| `custom_variables`     | No       | Values substituted into the agent's prompt. Max 64 keys, scalar values only, 30 KB total.       |
| `metadata`             | No       | Opaque JSON echoed back to you. Max 16 KiB.                                                     |
| `idempotency_key`      | No       | 1–128 chars of `[A-Za-z0-9_.:-]`.                                                               |
| `call_config`          | No       | Timeouts, retries and calling window — below.                                                   |

`custom_variables` values must be strings, numbers, booleans or null: they are substituted verbatim into the prompt, so a nested object would render as raw data inside the agent's instructions.

### `call_config`

| Field                                                                | Meaning                                                                                                                                                                                                                                                                     |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `idle_timeout_warning`                                               | Seconds of caller silence, counted from when the agent stops speaking, before the agent nudges. Must be less than `idle_timeout_end`, or the call ends before the nudge can play.                                                                                           |
| `idle_timeout_end`                                                   | Seconds of caller silence before the agent hangs up, counted from when the agent last stopped speaking. The nudge counts as agent speech and restarts this window, so a caller who never replies stays on the line for roughly `idle_timeout_warning` + `idle_timeout_end`. |
| `max_call_length`                                                    | Seconds; caps the whole call. **Max 60 minutes (3600).** Unset defaults to this ceiling; a higher value is rejected with `400`.                                                                                                                                             |
| `call_retry_config.retry_count`                                      | Extra dials after the first, `0`–`9`.                                                                                                                                                                                                                                       |
| `call_retry_config.retry_busy` / `retry_not_picked` / `retry_failed` | Delay before retrying a `busy` / `no_answer` / any other failed outcome, **in minutes**. Clamped to 1–1440.                                                                                                                                                                 |
| `call_time.call_start_time` / `call_end_time`                        | Permitted calling window, `HH:MM`.                                                                                                                                                                                                                                          |
| `call_time.timezone`                                                 | IANA zone the window is evaluated in.                                                                                                                                                                                                                                       |

A retry re-checks the calling window, so a backoff can never push a dial outside it. A `declined` call (the callee actively rejected it) is never retried.

### Fields that are refused

| Field          | Instead                                                                                                        |
| -------------- | -------------------------------------------------------------------------------------------------------------- |
| `config`       | Publish the agent with the config you want — for a non-human credential the published agent *is* the contract. |
| `webhook_url`  | Register a workspace [webhook subscription](/guides/webhooks#outbound-webhooks).                               |
| `scheduled_at` | Use `call_config.call_time`.                                                                                   |
| `call_type`    | `POST /calls` always places an outbound phone call.                                                            |
| `campaign_id`  | Use the [campaigns API](/concepts/campaigns).                                                                  |

<Note>
  `smart_formatter` (`extract_first_name`, `transliteration`, `transliteration_language`) is **accepted, stored and echoed back, but not applied** to the call. It exists for contract compatibility with migrating integrations; don't rely on it for behaviour.
</Note>

## Response

`POST /calls` answers **`200`** with the call record. Accepting a call is asynchronous — it may still be waiting for its calling window or queued behind your workspace's concurrency cap — so there is no dispatch id to report yet. Poll `GET /calls/{id}` or subscribe to `call_completed`.

```json theme={null}
{
  "call_id": "call_01EXAMPLE",
  "customer_id": "customer_01EXAMPLE",
  "agent_id": "agent_sales_hi",
  "status": "queued",
  "outcome": null,
  "sub_status": null,
  "error": null,
  "to_number_masked": "+9198****3210",
  "callee_name": "Jane",
  "from_number": "+911140000000",
  "from_phone_number_id": "pn_01EXAMPLE",
  "custom_variables": { "company": "Acme" },
  "call_config": { "max_call_length": 600 },
  "smart_formatter": {},
  "scheduled_for": null,
  "attempt_count": 0,
  "session_id": null,
  "created_at": "2026-08-11T09:00:00Z",
  "updated_at": "2026-08-11T09:00:00Z",
  "completed_at": null
}
```

The response never echoes `to_number` — you supplied it, and repeating it would put a subscriber number into every orchestrator's response logs. `to_number_masked` is enough to reconcile against your own record. There is no `participant_token`: this is a phone call, not a browser session.

## Lifecycle

```mermaid theme={null}
stateDiagram-v2
  [*] --> queued
  queued --> scheduled: outside the calling window
  scheduled --> dialing
  queued --> dialing
  dialing --> in_progress: answered
  dialing --> queued: retryable outcome, retries left
  in_progress --> completed
  dialing --> failed
  queued --> cancelled: /cancel
  in_progress --> cancelled: /cancel
```

`queued`, `scheduled`, `dialing` and `in_progress` are active; `completed`, `failed` and `cancelled` are terminal. Cancelling a terminal call returns `409`.

## Inspect a call

`GET /calls/{call_id}` returns the record above plus one `attempts` entry per dial:

```json theme={null}
{
  "call_id": "call_01EXAMPLE",
  "status": "completed",
  "attempt_count": 2,
  "attempts": [
    { "session_id": "session_01A", "status": "failed", "outcome": "no_answer", "sentiment": null, "started_at": "…", "ended_at": "…" },
    { "session_id": "session_01B", "status": "ended", "outcome": "completed", "sentiment": "positive", "started_at": "…", "ended_at": "…" }
  ]
}
```

Each `session_id` is a full [session](/concepts/sessions) — transcript, usage and recording all hang off it. `outcome` uses the same vocabulary everywhere in the platform: `completed`, `no_media`, `no_answer`, `busy`, `declined`, `failed`, `user_disconnected`, `expired`. `sentiment` is populated only for agents that opted into [sentiment analysis](/concepts/agents#post-call-sentiment).

`GET /calls` lists them, filtered by `status`, `agent_id`, `created_from` and `created_to`, with standard offset pagination.

## Idempotency

Send an `idempotency_key` and a repeat of the same request is refused with `409` naming the existing call, rather than dialling the number twice. This is the safe way to retry a `POST /calls` whose response you never saw.

## Errors

| HTTP | Code                   | When                                                                                                                      |
| ---- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| 400  | `invalid_request`      | Unknown or malformed field.                                                                                               |
| 402  | `insufficient_credits` | Workspace balance is below the invocation threshold, checked when the call is accepted. See [Billing](/concepts/billing). |
| 403  | `forbidden`            | Not an API-key credential, or the agent isn't yours.                                                                      |
| 404  | `not_found`            | Unknown `call_id` or agent.                                                                                               |
| 409  | `conflict`             | Duplicate `idempotency_key`, or cancelling a terminal call.                                                               |
| 503  | `service_unavailable`  | Call orchestration is temporarily unavailable; the call is marked failed and safe to retry.                               |

The `402` and destination checks run **before the call is queued**, so no call record is created on refusal. Where a deployment restricts outbound destinations, a `to_number` outside the permitted prefixes is refused with `400 invalid_request` ("to\_number is not a permitted destination").

<Note>
  Migrating an integration built against Ringg's individual-call endpoint? `/cu1/v1/calls` and `/cu1/v2/calls` accept that request shape unchanged and translate onto this API — see [Individual-call compatibility](/guides/individual-call-compatibility).
</Note>

<CardGroup cols={2}>
  <Card title="Outbound webhooks" icon="webhook" href="/guides/webhooks#outbound-webhooks">Get `call_completed` pushed to you instead of polling.</Card>
  <Card title="Sessions" icon="phone" href="/concepts/sessions">Transcript, usage and recording for each dial.</Card>
</CardGroup>
