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

# AI session events

> Five new ai_session.* events plus the stereo flag on call.recording_ready — for tenants running voice AI through Yotel.

When a call hands off to a [voice agent](/voice-agents/quickstart),
Yotel publishes a stream of webhook events that mirror the AI's
lifecycle: started, ended, transferred, escalated, and conference
changes. The four pre-existing `call.*` events (started, answered,
ended, recording\_ready) still fire — `call.recording_ready` gains a
`stereo` flag when the call was AI-driven.

This page is the reference for AI-call subscribers. For the global
event catalogue and delivery mechanics see
[Webhooks overview](/webhooks/overview) and
[Event catalog](/webhooks/event-catalog).

<Info>
  AI events ride the **same** delivery infrastructure as every other
  event — same envelope, same signature scheme, same retry schedule.
  Adding `ai_session.*` is a pure additive change; existing handlers
  keep working.
</Info>

## Common envelope

```json theme={null}
{
  "id": "evt_01HW8...",
  "type": "ai_session.transferred",
  "created_at": "2026-05-01T14:37:08.102Z",
  "tenant_id": "<uuid>",
  "data": { /* event-specific payload */ }
}
```

Headers:

```
X-Yotel-Signature: t=<unix_ts>,v0=<hex_hmac_sha256>
X-Yotel-Event-Id: evt_01HW8...
X-Yotel-Event-Type: ai_session.transferred
```

`X-Yotel-Signature` follows the Stripe shape:
`HMAC_SHA256(secret, f"{t}.{raw_body}")`. A 5-min freshness window
prevents replay. See [Signature verification](/webhooks/signature-verification)
for verifier code.

## Subscribing

Webhook subscriptions are managed from the dashboard, not the
public API. Pick AI-session events with the wildcard
`ai_session.*` to opt into the full stream:

```bash theme={null}
curl -X POST https://api.yotel.in/api/v1/webhooks \
  -H "Authorization: Bearer $YOTEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-host.example.com/webhooks/yotel",
    "event_types": ["ai_session.*", "call.recording_ready"]
  }'
```

Save the `signing_secret` from the response — it's shown once.

## Per-event reference

### `call.started`

Pre-answer originate event. Fires for every call, AI or human.
For AI campaigns, `voice_agent_id` is the resolved UUID.

```json theme={null}
{
  "call_id": "<uuid>",
  "direction": "outbound",
  "from": "+91...",
  "to": "+91...",
  "campaign_id": "<uuid|null>",
  "voice_agent_id": "<uuid|null>",
  "started_at": "2026-05-01T14:37:02.419Z"
}
```

### `call.answered`

Fires on `CHANNEL_ANSWER`. `answered_by_amd` is Yotel's AMD
classification — `"machine"` calls still get connected to the AI;
your AI service can decide whether to leave a message or hang up.

```json theme={null}
{
  "call_id": "<uuid>",
  "answer_time": "2026-05-01T14:37:08.102Z",
  "answered_by_amd": "human"
}
```

### `call.ended`

Fires on `CHANNEL_HANGUP`. `disposition` is `null` if the AI never
called `set_disposition`. For full AI lifecycle context see
`ai_session.ended` (fires alongside this one for AI calls).

```json theme={null}
{
  "call_id": "<uuid>",
  "end_time": "2026-05-01T14:39:02.102Z",
  "hangup_cause": "NORMAL_CLEARING",
  "total_duration_s": 120,
  "talk_duration_s": 114,
  "disposition": "interested"
}
```

### `call.recording_ready`

Fires when the WAV upload to GCS completes (typically +2-5s after
hangup). `stereo: true` for AI calls (left=caller, right=AI);
`false` for human-only calls.

```json theme={null}
{
  "call_id": "<uuid>",
  "recording_url": "https://api.yotel.in/api/v1/calls/<id>/recording",
  "signed_url": "https://storage.googleapis.com/.../<id>.wav?...",
  "duration_s": 47,
  "stereo": true,
  "format": "audio/wav"
}
```

<Warning>
  The `signed_url` expires \~15 minutes after issue. For long-lived
  access, hit `recording_url` (the proxy endpoint) on demand — or
  pull the bytes promptly into your own storage.
</Warning>

### `ai_session.started`

Fires when Yotel opens the audio fork to your voice agent.
`ws_url` is the resolved URL (after override hierarchy).

```json theme={null}
{
  "call_id": "<uuid>",
  "voice_agent_id": "<uuid>",
  "external_agent_id": "tier1_v3",
  "ws_url": "wss://ai.example.com/yotel/audio-fork",
  "started_at": "2026-05-01T14:37:08.500Z"
}
```

### `ai_session.ended`

Fires when the AI session terminates (transfer, hangup, error,
or 30-min timeout). `metadata` carries the full audit log —
every `log` invocation plus the final transfer/hangup metadata.

```json theme={null}
{
  "call_id": "<uuid>",
  "voice_agent_id": "<uuid>",
  "ended_at": "2026-05-01T14:39:01.999Z",
  "outcome": "transferred_agent",
  "metadata": {
    "logs": [ {"step": "intent_classified", "intent": "renewal"} ],
    "final": {"destination_type": "agent_queue", "destination": "tier1"}
  }
}
```

`outcome` is one of: `transferred_agent`, `transferred_e164`,
`transferred_sip_uri`, `hangup`, `error`, `timeout`.

### `ai_session.transferred`

Fires on a successful `transfer` verb. The AI's leg drops; the
caller is bridged to `destination`. `ai_session.ended` fires
immediately after.

```json theme={null}
{
  "call_id": "<uuid>",
  "voice_agent_id": "<uuid>",
  "destination_type": "agent_queue",
  "destination": "tier1",
  "transferred_at": "2026-05-01T14:39:01.999Z",
  "metadata": { "intent": "billing", "summary": "..." }
}
```

### `ai_session.escalated`

Fires on `request_supervisor` invocation **and** again on
supervisor claim. Subscribers should dedupe on `event.id` if they
only want the terminal claim.

```json theme={null}
{
  "call_id": "<uuid>",
  "voice_agent_id": "<uuid>",
  "reason": "Caller is angry",
  "urgency": "high",
  "supervisor_id": null,
  "escalated_at": "2026-05-01T14:38:30.000Z"
}
```

| `supervisor_id` | Phase                                          |
| --------------- | ---------------------------------------------- |
| `null`          | Invocation — escalation enqueued, no human yet |
| `<uuid>`        | Claim — supervisor accepted from the dashboard |

### `ai_session.conference_changed`

Fires on every conference state transition (`conference_start`,
`add`, `remove`, `leave`) and the supervisor verbs (`whisper`,
`barge`, `monitor_start`, `monitor_stop`).

```json theme={null}
{
  "call_id": "<uuid>",
  "voice_agent_id": "<uuid>",
  "conference_id": "<uuid>",
  "change": "added",
  "actor": "ai",
  "participants": [
    {"member_id": "mem-1", "role": "caller", "destination": "+9198..."},
    {"member_id": "mem-2", "role": "ai", "destination": "<voice-agent-id>"},
    {"member_id": "mem-3", "role": "agent", "destination": "ag-7"}
  ],
  "changed_at": "2026-05-01T14:38:15.420Z"
}
```

`change` is one of: `started`, `added`, `removed`, `left`,
`monitor_started`, `monitor_stopped`, `whisper`, `barge`.
`actor` is `"ai"`, `"supervisor"`, or `"tenant_api"`.

## Stereo recording flag

For AI-driven calls, `RECORD_STEREO=true` is set on the FreeSWITCH
dialplan. The resulting WAV has two channels:

* **Left** — the caller's audio (PSTN side).
* **Right** — the AI's audio (synthesised TTS or pre-recorded URL
  payback returned over the WS).

Mono recordings are returned for human-only calls (no
`connect_voice_agent` step). The `stereo` flag on
`call.recording_ready` lets transcript/QA pipelines pick the right
diarisation strategy without probing the file.

## SDK signature verification

Both SDKs already ship a webhook helper — no new function for
`ai_session.*`. The `WebhookEventType` union grew by 5 (and 1 field
on `RecordingReady`); upgrade the SDK to get the new types.

```python Python theme={null}
import yotel

event = yotel.webhook.verify_and_parse(
    raw_body,
    headers=request.headers,
    signing_secret=os.environ["YOTEL_WEBHOOK_SECRET"],
)
if event.type == "ai_session.transferred":
    ...
```

```typescript TypeScript theme={null}
import { verifyWebhook } from "@yotel/client";

const event = verifyWebhook({
  rawBody: req.body,
  headers: req.headers,
  signingSecret: process.env.YOTEL_WEBHOOK_SECRET!,
});
if (event.type === "ai_session.transferred") {
  // event.data is fully typed
}
```

## Retry & dead-letter

Inherited from the global delivery layer:

* 3 retries with backoff `200ms → 2s → 20s`.
* After 3 failures the delivery is marked `dead_lettered`.
* Replay manually via
  `POST /api/v1/webhooks/deliveries/{id}/replay`.
* 2xx = success; 4xx (except 408/429) = no retry; 5xx + 408/429 retry.

See [Retries](/webhooks/retries) for the full schedule.

## See also

* [Voice agents quickstart](/voice-agents/quickstart)
* [Control API](/voice-agents/control-api) — verbs that produce
  these events.
* [Event catalog](/webhooks/event-catalog) — every event Yotel can
  publish.
* [Signature verification](/webhooks/signature-verification) —
  reference verifier implementations.
