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

# Sessions

> A single voice call — web or outbound phone — from creation to termination.

A **session** is one voice call: a media room, a dispatched agent, a participant, and the transcript/usage recorded when it ends. Every voice conversation is a session, whichever way it started — a browser client, an API-fired [call](/concepts/calls), a [campaign](/concepts/campaigns) attempt or an inbound call.

## Create a session

`POST /sessions` is authenticated with a **bearer login session** (not an API key) and `sessions:write`, because the session is attached to the logged-in user.

<Note>
  To place an outbound phone call from your **backend**, use [`POST /calls`](/concepts/calls) instead — it takes an API key, and adds retries, calling windows and idempotency. `POST /sessions` is the browser/dashboard path.
</Note>

| Field                  | Required | Notes                                                                           |
| ---------------------- | -------- | ------------------------------------------------------------------------------- |
| `agent_id`             | Yes      | A **published** agent from the catalog.                                         |
| `call_type`            | No       | `web` (default) or `outbound_phone`.                                            |
| `phone_number`         | No       | E.164 number to dial for `outbound_phone`.                                      |
| `participant_identity` | No       | Defaults to the auth user ID.                                                   |
| `participant_name`     | No       | Defaults to the logged-in user's name.                                          |
| `custom_variables`     | No       | Values substituted into the agent's prompt.                                     |
| `config`               | No       | Per-session provider overrides — see [Providers & Models](/concepts/providers). |
| `metadata`             | No       | Client metadata (trusted IDs are server-stamped).                               |

The response is a `SessionRecord` that **includes the `participant_token`** — this is the only place the token is returned. Connect it to `room_name` at `livekit_url` with a voice client to join the call.

<Warning>
  `customer_id`, `user_id`, `agent_id`, and `session_id` are stamped server-side and overwrite any client-supplied metadata. Participant tokens never appear in list/get responses.
</Warning>

## Web vs outbound phone

* **Web** — the browser participant connects to the room and talks to the agent. The token grants publish + subscribe.
* **Outbound phone** — the agent dials the recipient over SIP; the token is subscribe-only (the human is on the phone leg). Requires an outbound SIP trunk to be configured on the deployment.

## Status lifecycle

```mermaid theme={null}
stateDiagram-v2
  [*] --> active: create (dispatch + token)
  active --> ended: /end, room finished, or status PATCH
  active --> failed: failure / status PATCH
  active --> expired: expiry sweep (older than TTL)
  ended --> [*]
  failed --> [*]
  expired --> [*]
```

* `ended_at` is set on the transition to any terminal status.
* Long-running `active` sessions are swept to `expired` after the session TTL (default 24h).
* **Terminal status is frozen.** Once a session is `ended`, `failed`, or `expired`, its `status` and `ended_at` stop changing. A `PATCH /sessions/{id}/status` on a terminal session is a silent no-op — `200` with the record unchanged, not an error.
* **Ending tears down the room.** `POST /sessions/{id}/end` (or a PATCH that drives the session terminal) deletes the media room, so an explicit end actually stops a call that would otherwise keep billing.

## Manage a session

| Method | Path                            | Scope                                             |
| ------ | ------------------------------- | ------------------------------------------------- |
| POST   | `/sessions`                     | **bearer** + `sessions:write`                     |
| GET    | `/sessions`                     | `sessions:read` (filter by `user_id`, `agent_id`) |
| GET    | `/sessions/{session_id}`        | `sessions:read`                                   |
| POST   | `/sessions/{session_id}/end`    | `sessions:write`                                  |
| PATCH  | `/sessions/{session_id}/status` | `sessions:write`                                  |

<Note>
  `GET /sessions` is paged: pass `?limit=` (default 20, max 100) and `?offset=`, and read `pagination.total` from the `{ "data": [...], "pagination": { ... } }` envelope.
</Note>

## Results

When a call ends, the agent posts a [session-update webhook](/guides/webhooks) with the transcript, usage, and a session report — merged into the session record and used for [billing](/concepts/billing). For production, register an [outbound webhook](/guides/webhooks#outbound-webhooks) and receive `call_completed` rather than polling.

Two fields are written during that finalize step:

* **`outcome`** — how the call ended: `completed`, `no_media`, `no_answer`, `busy`, `declined`, `failed`, `user_disconnected` or `expired`. It's the same vocabulary campaigns use to decide retries and [analytics](/concepts/analytics) uses to aggregate.
* **`sentiment`** — `positive`, `negative` or `neutral`, and only for agents that opted into [sentiment analysis](/concepts/agents#post-call-sentiment).

<CardGroup cols={2}>
  <Card title="Webhooks & Events" icon="bell" href="/guides/webhooks">Receive transcript and usage.</Card>
  <Card title="Campaigns" icon="users" href="/concepts/campaigns">Run sessions in bulk.</Card>
</CardGroup>
