> ## 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.

# Individual-call compatibility API

> Point an existing Ringg individual-call integration at Lehar by changing only the host, key and base path.

If you already have an integration built against Ringg's **Initiate Individual Call** endpoint, `/cu1/v1/calls` and `/cu1/v2/calls` accept that exact request shape and return that exact response envelope. Migrating means changing three things — host, API key, base path — and nothing else in your code.

<Note>
  This is a **compatibility surface**, not the primary API. For a new integration use [`POST /calls`](/concepts/calls) instead: same engine underneath, but native field names, richer responses, and endpoints for listing, inspecting and cancelling a call — none of which exist here.
</Note>

## The endpoints

| Method | Path            | Auth / Scope                            |
| ------ | --------------- | --------------------------------------- |
| POST   | `/cu1/v1/calls` | **`X-API-KEY` only** + `sessions:write` |
| POST   | `/cu1/v2/calls` | **`X-API-KEY` only** + `sessions:write` |

Both paths run the **same handler** and behave identically. v2 exists only so a client already pointed at a v2 URL doesn't have to change; its `number_pool_id` is accepted and ignored, because Lehar has no managed number pools.

Like [`POST /calls`](/concepts/calls), these reject bearer logins and agent-session tokens even though both can carry `sessions:write`.

```bash theme={null}
curl -X POST "$LEHAR_BASE_URL/cu1/v1/calls" \
  -H "X-API-KEY: $LEHAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Jane",
    "mobile_number": "+919876543210",
    "agent_id": "agent_sales_hi",
    "from_number": "+911140000000",
    "custom_args_values": { "company": "Acme" },
    "metadata": { "candidate_id": "c_8f21", "source": "voice_campaign" }
  }'
```

```json theme={null}
{
  "status": "success",
  "message": "Call initiated successfully",
  "data": {
    "call_id": "call_01EXAMPLE",
    "call_direction": "outbound",
    "call_status": "registered",
    "initiated_at": "2026-08-11T09:00:00Z",
    "agent_id": "agent_sales_hi",
    "custom_args_values": { "company": "Acme", "callee_name": "Jane", "mobile_number": "+919876543210" }
  }
}
```

<Note>
  Success uses this envelope; **pre-flight rejections do not**. A workspace out of credits returns a native `402 insufficient_credits`, and — where the deployment restricts destinations — a `mobile_number` outside the allowed prefixes returns a native `400 invalid_request`, both shaped as `{ "error": { "code", "message" } }` rather than `{ "status": "success", … }`. This matches the `404` an unknown `from_number` already returns.
</Note>

## How fields map

Every request is translated onto a native `POST /calls` body, then validated by the native endpoint — so the two contracts can't drift apart.

| Compatibility field  | Native equivalent               | Notes                                                                                                                                                                                                                     |
| -------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`               | `callee_name`                   | Required.                                                                                                                                                                                                                 |
| `mobile_number`      | `to_number`                     | Required, E.164.                                                                                                                                                                                                          |
| `agent_id`           | `agent_id`                      | Required; must be a **published** voice agent.                                                                                                                                                                            |
| `from_number_id`     | `from_phone_number_id`          | A number id **or an E.164 number** — resolved by what it contains, not by the field name. A number is looked up against your workspace (`404` if it isn't one); an id is used as-is. Takes precedence over `from_number`. |
| `from_number`        | `from_phone_number_id`          | An E.164 string **or a list** (first usable one wins), resolved to one of your workspace numbers. `404` if it isn't one.                                                                                                  |
| `custom_args_values` | `custom_variables`              | Same semantics — values substituted into the prompt. Scalars only (string, number, boolean, `null`); at most 64 keys and 30 KB.                                                                                           |
| `call_config`        | `call_config`                   | Idle timeouts, retries, calling window. Both the flat and the nested layout are accepted — see below.                                                                                                                     |
| `call_retry_config`  | `call_config.call_retry_config` | Accepted at the **top level** of the body and folded in — see below.                                                                                                                                                      |
| `metadata`           | `metadata.cu1.metadata`         | Your own opaque bag, echoed verbatim on every webhook — see below.                                                                                                                                                        |
| `smart_formatter`    | `smart_formatter`               | Accepted and stored, **not applied**.                                                                                                                                                                                     |

### Reserved prompt variables

`name` and `mobile_number` are also made available to the prompt as `{{callee_name}}` and `{{mobile_number}}`, matching the source contract. They are defaults, not overrides — if you pass either key inside `custom_args_values`, your value wins.

### Calling window and retries

Older integrations put the calling window directly on `call_config` and send `call_retry_config` as a **sibling** of it, rather than nesting both:

```json theme={null}
{
  "call_config": { "call_start_time": "09:00", "call_end_time": "18:00", "timezone": "Asia/Kolkata" },
  "call_retry_config": { "retry_count": 2 }
}
```

Both layouts work. That body is folded into the nested shape the engine reads — the window under `call_config.call_time`, the retry policy under `call_config.call_retry_config` — so your window and retry count are honoured rather than quietly ignored. The fold is driven by the shape of what you send, not by which path you call, so a body that already nests is untouched. If a body somehow carries both, the nested form wins.

### Correlating with your own records

`metadata` is an **opaque passthrough**. Send any JSON object — your own ids, your own key names, no schema — and it is stored on the call and echoed back **unchanged** on every webhook event as a top-level `metadata` field. That is how you line an event up against your own database rows without keeping a map of our call ids.

Nothing interprets it, nothing requires particular keys, and it is never merged into the agent's runtime settings. It is not returned on the POST response — the response mirrors the source contract, which echoes `custom_args_values` only. Capped at 16 KiB, counted together with the unsupported fields stashed alongside it.

### Response status vocabulary

`call_status` is the compatibility vocabulary, not Lehar's native call status:

| `call_status` | Native `status`          |
| ------------- | ------------------------ |
| `registered`  | `queued`, `scheduled`    |
| `ongoing`     | `dialing`, `in_progress` |
| `completed`   | `completed`              |
| `failed`      | `failed`                 |
| `cancelled`   | `cancelled`              |

## Fields that are accepted but do nothing

The contract is deliberately **lenient**: a field Lehar can't honour is stored rather than rejected, so an unimplemented setting never breaks a migrating caller mid-cutover.

`voice_id`, `version_id`, `call_category`, `parent_call_id`, `scheduled_at`, `webhook_url`, `language`, v2's `number_pool_id` — and anything else unrecognised — are preserved under `metadata.cu1` on the call record for traceability, nested so they can never act as a dispatch override.

Your own `metadata` lands in the same place (at `metadata.cu1.metadata`) but is **not** one of these: it is honoured, and echoed on every webhook. The nesting is for a different reason — the top level of a call's metadata is merged into the agent's dispatch settings, so an un-nested bag could override runtime configuration.

<Warning>
  This is the one place the two APIs differ in spirit. Native [`POST /calls`](/concepts/calls) **rejects** an unknown field with a `400` naming it, precisely so a setting can't appear to take effect when it didn't. Here it is silently kept and ignored. If you need `scheduled_at` behaviour, use `call_config.call_time`; for per-call voice or config, publish an agent with that config.
</Warning>

## What this surface doesn't have

There is no compatibility endpoint for reading calls. Use the native ones — they work on the same records, and `call_id` from the response above is the id they take:

* `GET /calls/{call_id}` — status, outcome, and one entry per dial.
* `POST /calls/{call_id}/cancel` — stop a call before the next dial.
* [Outbound webhooks](/guides/webhooks#outbound-webhooks) — `call_completed` with transcript, cost and sentiment.

## Receiving the result

The request side is compatible by translation; the result side is compatible by configuration. Register a subscription per agent with:

```json theme={null}
{
  "agent_id": "agent_...",
  "url": "https://hooks.example.com/lehar",
  "headers": { "Authorization": "Bearer your-receiver-token" },
  "payload_format": "ringg"
}
```

That gives you the three things a migrating receiver needs. `headers` are replayed verbatim, so your existing static-header check keeps working and signature verification is not required. `payload_format: "ringg"` reshapes the body — `transcript` as an array of `{"bot": …}` / `{"user": …}` turns, Ringg's `status`/`sub_status` vocabulary, a 24-hour `recording_url`, and `retry_count`/`attempts` on `call_completed`. And because subscriptions are per agent, per-agent routing works the same way it does today.

Your `metadata` comes back verbatim on every event, which is the piece that makes a migrating receiver's correlation logic work unchanged.

Three differences remain deliberately:

* `to_number` is **masked**.
* `version_id`, `version_slug`, `version_description` and `tool_call_logs` are not sent — Lehar has no equivalent.
* `custom_args_values` is **not echoed**. Those are your prompt variables, and an integration that ships whole prompt blocks through them would get tens of KB of its own text back on three events per call. Put correlation keys in `metadata`; read the prompt variables from `GET /calls/{call_id}` if you need them.

Note also that a **new agent starts with no webhook** — register one for each agent you care about.

<CardGroup cols={2}>
  <Card title="Calls" icon="phone-arrow-up-right" href="/concepts/calls">The native API this translates onto.</Card>
  <Card title="Outbound webhooks" icon="webhook" href="/guides/webhooks#outbound-webhooks">Receive the result of the call.</Card>
</CardGroup>
