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

# Control API

> All 25 mid-call verbs your voice AI can invoke against Yotel — transfer, hangup, hold, conference, request_supervisor, schedule_callback, and more.

`POST /api/v1/ai-sessions/{call_id}/control` is the single endpoint
your voice AI uses to drive a live call. The JSON body's `event`
field is the discriminator — there are 25 verbs across three groups:
**call-state primitives** (13), **conferencing + supervisor** (9), and
**lead / context** (3).

## Authentication

| Credential                                  | When to use                                                                                        | Scope                       |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------- | --------------------------- |
| Per-call callback token (`yt_cb_<26 ULID>`) | Production — your voice AI gets one in the WS metadata frame, valid 30 min, bound to one `call_id` | `voice_agent:control`       |
| Tenant API key                              | Admin / dev / testing — explicit opt-in only                                                       | `ai_sessions:control_admin` |

The callback token auto-rotates at minute 25 of the call via a
`token_refresh` text frame on the WS. Agents that ignore the refresh
lose control-verb access at the 30-min mark; the call continues
without interruption.

<Warning>
  **`ai_sessions:control_admin` is opt-in.** Tenant API keys do **not**
  carry control-verb permission by default. Add the scope to a key
  explicitly when you want to drive calls from a dashboard or test
  harness.
</Warning>

## Request envelope

```http theme={null}
POST /api/v1/ai-sessions/{call_id}/control HTTP/1.1
Authorization: Bearer <yt_cb_... | yt_live_...>
Content-Type: application/json
Idempotency-Key: <uuid v4>

{ "event": "<verb>", ...verb-specific fields }
```

* **Tenant fence.** `call_id` must belong to the token's tenant.
  Cross-tenant access returns 403.
* **Rate limit.** 10 req/sec per `call_id`.
* **Idempotency.** The Yotel SDKs auto-generate an `Idempotency-Key`
  for state-changing verbs. Replays of the same `(call_id, key)` pair
  return the cached response. See [Idempotency](/concepts/idempotency).

## Response envelope

```json theme={null}
{
  "ok": true,
  "call_id": "<uuid>",
  "dispatched_at": "2026-05-01T14:37:08.102Z",
  "result": {}
}
```

`result` carries verb-specific data (e.g. `{"conference_id": "..."}`)
for verbs that have a return; empty `{}` otherwise.

## Group 1 — Call-state primitives (13)

Operate on the AI's own call leg.

### `transfer`

Bridge the caller to an agent queue, an E.164 number, or a SIP URI.
The AI's leg drops; `outcome ← transferred_*`. Fires
`ai_session.transferred` + `ai_session.ended`.

```json theme={null}
{ "event": "transfer", "destination_type": "agent_queue",
  "destination": "tier1", "metadata": { "intent": "billing" } }
```

```python Python theme={null}
client.ai_sessions.transfer(
    call_id, destination_type="agent_queue", destination="tier1",
    metadata={"intent": "billing"}, callback_token=cb_token,
)
```

### `hangup`

End the call. `outcome ← hangup`; fires `ai_session.ended`.

```json theme={null}
{ "event": "hangup", "metadata": { "reason": "task_complete" } }
```

```typescript TypeScript theme={null}
await client.aiSessions.hangup(callId, {
  metadata: { reason: "task_complete" },
  callbackToken,
});
```

### `log`

Append a structured entry to `ai_sessions.metadata.logs[]`. No
state effect, no webhook — useful for breadcrumbs in the audit log.

```json theme={null}
{ "event": "log", "metadata": { "step": "intent_classified",
  "intent": "renewal" } }
```

```python Python theme={null}
client.ai_sessions.log(call_id, metadata={"step": "intent_classified"})
```

### `mute` / `unmute`

Mute one party for the rest of the call.
`target` is `"caller"` (default) or `"agent"` (the AI's leg).

```json theme={null}
{ "event": "mute", "target": "caller" }
```

```python Python theme={null}
client.ai_sessions.mute(call_id, target="caller")
client.ai_sessions.unmute(call_id, target="caller")
```

### `hold` / `unhold`

Place the caller on hold; optional `moh_url` for music. `unhold`
clears the state.

```json theme={null}
{ "event": "hold", "moh_url": "https://cdn.example.com/moh.wav" }
```

```typescript TypeScript theme={null}
await client.aiSessions.hold(callId, { moh_url: mohUrl, callbackToken });
await client.aiSessions.unhold(callId, { callbackToken });
```

### `send_dtmf`

Inject DTMF digits into the call (e.g. when navigating an external
IVR after a transfer attempt).

```json theme={null}
{ "event": "send_dtmf", "digits": "1234#" }
```

```python Python theme={null}
client.ai_sessions.send_dtmf(call_id, digits="1234#")
```

### `play_audio`

Play either a pre-recorded URL **or** TTS text — exactly one of
`audio_url` / `tts_text` must be set. See the [TTS limitation](#tts-not-yet-supported) below.

```json theme={null}
{ "event": "play_audio",
  "audio_url": "https://cdn.example.com/prompts/greeting.wav" }
```

```python Python theme={null}
client.ai_sessions.play_audio(call_id, audio_url=greeting_url)
```

### `set_disposition`

Save a disposition code on the call. `source` is recorded as `"ai"`.

```json theme={null}
{ "event": "set_disposition", "disposition": "interested",
  "notes": "Asked for callback Tue 2pm" }
```

```python Python theme={null}
client.ai_sessions.set_disposition(
    call_id, disposition="interested", notes="callback Tue 2pm"
)
```

### `recording_pause` / `recording_resume`

Stop/start recording mid-call (e.g. PCI compliance during card
capture). Optional `reason` is stored on the audit log.

```json theme={null}
{ "event": "recording_pause", "reason": "pci" }
```

```python Python theme={null}
client.ai_sessions.recording_pause(call_id, reason="pci")
client.ai_sessions.recording_resume(call_id)
```

### `get_call_state`

Read-only snapshot of current state. No idempotency key needed.

```json theme={null}
{ "event": "get_call_state" }
```

Returns:

```json theme={null}
{ "ok": true, "result": {
    "state": "in_progress", "duration_s": 47, "answered": true,
    "current_participants": ["caller", "ai_agent"],
    "is_recording": true,
    "is_muted": {"caller": false, "agent": false},
    "is_held": false, "conference_id": null, "disposition": null
}}
```

## Group 2 — Conferencing + supervisor (9)

Add other participants to the call, monitor or barge into another
call leg, or escalate to a human supervisor.

### `conference_start`

Promote the AI's two-leg call into a conference. The AI stays in.
Fires `ai_session.conference_changed`.

```json theme={null}
{ "event": "conference_start" }
```

```python Python theme={null}
client.ai_sessions.conference_start(call_id)
```

### `conference_add`

Dial a participant into the conference. `participant_type` is one of
`e164 | sip_uri | agent | supervisor`.

```json theme={null}
{ "event": "conference_add", "participant_type": "agent",
  "destination": "ag-7", "metadata": {"reason": "warm_handoff"} }
```

```python Python theme={null}
client.ai_sessions.conference_add(
    call_id, participant_type="agent", destination="ag-7"
)
```

### `conference_remove`

Kick a participant by `member_id`. `member_id` comes from the
`participants[]` array on `ai_session.conference_changed`.

```json theme={null}
{ "event": "conference_remove", "member_id": "mem-42" }
```

### `conference_leave`

The AI's leg drops; the rest of the conference continues.

```json theme={null}
{ "event": "conference_leave" }
```

### `request_supervisor`

Enqueue a supervisor escalation. UI alert + WebSocket push to
on-duty supervisors. Fires `ai_session.escalated` **twice**: once on
invocation (`supervisor_id` null), once on claim.

```json theme={null}
{ "event": "request_supervisor",
  "reason": "Caller is angry, needs senior rep",
  "urgency": "high" }
```

```python Python theme={null}
client.ai_sessions.request_supervisor(
    call_id, reason="Caller is angry", urgency="high"
)
```

### `whisper`

One-way audio injection into another call leg (typically an agent's
ear during a coaching session). Requires `target_call_id` and an
existing monitor or conference relationship — bare invocation on an
unrelated call returns 403.

```json theme={null}
{ "event": "whisper", "target_call_id": "<agent-call-uuid>",
  "audio_url": "https://cdn.example.com/coach/upsell-tip.wav" }
```

```python Python theme={null}
client.ai_sessions.whisper(
    call_id, target_call_id=agent_call_id, audio_url=tip_url
)
```

### `barge`

Promote from monitor-only to a full participant in `target_call_id`.
Both sides hear the AI.

```json theme={null}
{ "event": "barge", "target_call_id": "<agent-call-uuid>" }
```

### `monitor_start` / `monitor_stop`

Open or close a one-way audio fork from a target call into the AI's
WS. `mode` is `listen` (default) or `listen_and_whisper`.

```json theme={null}
{ "event": "monitor_start",
  "target_call_id": "<agent-call-uuid>", "mode": "listen" }
```

```python Python theme={null}
client.ai_sessions.monitor_start(
    call_id, target_call_id=agent_call_id, mode="listen"
)
client.ai_sessions.monitor_stop(call_id, target_call_id=agent_call_id)
```

## Group 3 — Lead / context (3)

Persist data back to the lead row or schedule follow-up work.

### `set_lead_field`

Patch one custom field on the lead's `custom_fields` JSONB.

```json theme={null}
{ "event": "set_lead_field", "field": "preferred_callback_window",
  "value": "weekday_evenings" }
```

```python Python theme={null}
client.ai_sessions.set_lead_field(
    call_id, field="preferred_callback_window", value="weekday_evenings"
)
```

### `set_lead_status`

Override the lead status. Recorded as `status_set_by='ai'`.

```json theme={null}
{ "event": "set_lead_status", "status": "qualified" }
```

```python Python theme={null}
client.ai_sessions.set_lead_status(call_id, status="qualified")
```

### `schedule_callback`

Insert a `callbacks` row. The campaign engine picks it up at
`scheduled_at`. Optional `voice_agent_id` to route the callback to a
different agent (e.g. escalate Tier-1 → Tier-2).

```json theme={null}
{ "event": "schedule_callback",
  "scheduled_at": "2026-05-03T14:00:00Z",
  "reason": "Customer requested Tuesday 2pm",
  "voice_agent_id": "<tier2-agent-id>" }
```

```python Python theme={null}
client.ai_sessions.schedule_callback(
    call_id, scheduled_at="2026-05-03T14:00:00Z",
    reason="callback requested",
)
```

## Idempotency

`Idempotency-Key` is honored on every state-changing verb (read-only
`log` and `get_call_state` ignore it). The Python and TypeScript SDKs
auto-generate a UUID v4 per call so retries are safe by default;
override with `idempotency_key=...` (Python) / `idempotencyKey: ...`
(TS) when you want cross-process dedup.

| Class of verb                                                 | Behaviour                                                                                                                |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `transfer`, `hangup`, `conference_*`                          | Strongly idempotent — second invocation hits a state-machine 409, but a replay with the same key returns the cached 200. |
| `log`, `set_lead_field`, `set_disposition`, `set_lead_status` | Last-write-wins; idempotency key just suppresses retry storms.                                                           |
| `play_audio`, `whisper`, `send_dtmf`                          | Without a key, replays replay-the-action. Key dedupes within a 60s window.                                               |

Replay window: 24h on `(call_id, idempotency_key)`.

## TTS not yet supported

`play_audio` and `whisper` accept a `tts_text` field in the v1
protocol, but the dispatcher currently returns **422** when that
shape is sent — TTS rendering isn't wired in v1. Use `audio_url`
with a pre-rendered WAV/MP3 for now. TTS support is tracked for
v1.1.

## Error reference

| HTTP | SDK class (Python / TS)                             | Typical cause                                                                                                    |
| ---- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| 400  | `ValidationError` / `ValidationError`               | Body schema invalid (e.g. both `audio_url` and `tts_text` set, missing `target_call_id`)                         |
| 401  | `AuthenticationError` / `AuthenticationError`       | Callback token expired or bad signature                                                                          |
| 403  | `PermissionDenied` / `PermissionDenied`             | Tenant fence (call\_id ≠ token tenant), missing scope, or `target_call_id` lacks monitor/conference relationship |
| 404  | `NotFoundError` / `NotFoundError`                   | Call doesn't exist                                                                                               |
| 409  | `ConflictError` / `ConflictError`                   | Call already terminated, conference state mismatch, or `voice_agent` in use during delete                        |
| 422  | `ValidationError`                                   | TTS not wired (use `audio_url`)                                                                                  |
| 424  | `NoVoiceAgentConfigured` / `NoVoiceAgentConfigured` | Override hierarchy resolved no agent — set a tenant default or pin per campaign/flow                             |
| 429  | `RateLimitedError` / `RateLimitedError`             | 10 req/sec/call\_id exceeded, or `max_concurrent` cap on the agent hit. `retry_after_s` returned                 |
| 5xx  | `ServerError` / `ServerError`                       | Yotel-internal — `request_id` returned for support                                                               |

## See also

* [Voice agents quickstart](/voice-agents/quickstart) — set up the
  routing alias before invoking control verbs.
* [AI session webhook events](/webhooks/ai-session-events) — async
  events that fire as a side-effect of these verbs.
* [Authentication & scopes](/authentication/scopes) — full scope
  catalogue including `voice_agents:*` and `ai_sessions:*`.
