# Start a speech-to-speech session (audio only, no video)

URL: https://www.atmee.ai/docs/v2/api/reference/speech-sessions/createSpeechSession

`POST /v1/speech_sessions`

Reserves a **speech session** with an avatar: a realtime
speech-to-speech conversation over a WebSocket, without video. The
answer carries the WebSocket `url` and a short-lived `token`:
connect to `url` within 60 seconds (`tokenExpiresAt`), presenting the
token either as `Authorization: Bearer <token>` or as the WebSocket
subprotocols `atmee-live` and `atmee-token.<token>`. The token only
admits the connection. The session ends at its `endsAt` (the
reservation plus `actualDurationSeconds`), whenever the connection
opened. The first connection holds the session for good: no other
connection can use it, so a client that lost its connection starts a
new session.

`avatarId` is required: the conversation uses that avatar's persona
(name, personality, facts, self-introduction) and always its voice;
an avatar without a voice answers `422
avatar_has_no_voice`.

`settings` pins `session.start` fields from your backend: the speech
server applies them over the client's, refuses a client
`session.start` / `session.update` that sets a pinned field (naming
it), and leaves them out of the session it echoes. Unpinned fields
stay the client's to set. Pinned persona fields override the
avatar's. Appends (`session.instructions.append` and the like) stay
allowed: they add to the pinned instructions, never replace them.

Billing runs by the minute from the speech server's first usage report
(within 30 s of the connection) until the session ends, at the
plan's speech rate. A session nobody connects to costs nothing and is
closed (`failed`, `no_user_joined`) by the first read, or start of
the account, 30 seconds after its token expired, otherwise by the
stale-session cleanup: 120 seconds without a heartbeat (the
reservation sets the first), checked every minute.

**Who pays.** Always the account that owns the avatar:

- **API key** (`X-Api-Key`) — your backend, billed to the key's
  account. Requires the `sessions` scope; an avatar-scoped key may
  only start sessions on its avatar (it must name it).
- **The owner's login** (`Authorization: Bearer <jwt>`, where the
  deployment accepts user tokens) — billed to the account the user
  acts for (`X-Account-Id` when there are several). An avatar of
  another account answers `403 avatar_not_allowed`.
- **No credential** — `401`. There is no anonymous or visitor-paid
  speech session.

With both an API key and a bearer token, the key's start applies,
as on `POST /v1/sessions`. Read and end a session with either
credential of its account.

## Request body

- `application/json`: `SpeechSessionCreateRequest`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `avatarId` | string | yes | An avatar of this account, with a voice, whose persona and voice the session uses (an avatar-scoped key's own avatar). |
| `maxDurationSeconds` | integer | no | The session's length in seconds (default 600), counted from the reservation (this call), whenever the connection opens. Shortened when the balance or the plan allows less; the answer says what was granted (`actualDurationSeconds`). |
| `language` | string | no | BCP 47 language of the conversation (e.g. `en`, `fr-FR`); passed to the speech server as given. |
| `settings` | object | no | `session.start` fields pinned by your backend: the client cannot set them (see the operation). |

## Responses

| Status | Description | Schema |
| --- | --- | --- |
| `201` | The session is reserved; connect to `url` with `token`. | `SpeechSessionCreateResponse` |
| `400` | Both `X-Api-Key` and `Authorization` sent where the deployment accepts user tokens; send one. | none |
| `401` | Missing or invalid API key -- or a user token that is invalid, anonymous, or of a user who may act for no account (invalid_login) | none |
| `402` | Out of credits, this key's spend cap was reached, or too many sessions at once for this key or plan | `PaymentRequiredResponse` |
| `403` | `insufficient_scope` — the API key lacks the `sessions` scope \| `avatar_not_allowed` — the avatar is not this account's, or the key is scoped to another avatar (an avatar-scoped key must name its avatar) \| `api_access_disabled` \| account_not_allowed (a user token whose X-Account-Id names an account the user may not act for) | `ApiErrorResponse` |
| `409` | account_ambiguous -- a user token of a user who may act for several accounts, without an `X-Account-Id` header naming one. | `ApiErrorResponse` |
| `422` | The body failed schema validation (`FrameworkErrorResponse`, e.g. no `avatarId`), or (`ApiErrorResponse`) `avatar_has_no_voice` — the avatar has no voice to speak with; `invalid_settings` — `settings` breaks one of its limits (the message names the field). | object |
| `503` | `speech_unavailable` — speech sessions are not configured on this deployment, or its database does not support them yet. `settings_unavailable` — the avatar could not be read; retry. `embed_disabled` — API-key sessions are switched off. | `ApiErrorResponse` |

## `201` response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `links` | object | no |  |
| `sessionId` | string | yes |  |
| `url` | string | yes | The WebSocket URL to connect to (`wss://...`). |
| `token` | string | yes | Admits one connection to this session; valid for 60 seconds. Not a credential for anything else. |
| `tokenExpiresAt` | string | yes |  |
| `actualDurationSeconds` | integer | yes | The length granted by the key's, the plan's and the balance's limits, counted from the reservation (this call), whenever the connection opens; the session ends at the reservation plus this many seconds (`endsAt` on GET). |
| `requestedDurationSeconds` | integer | no | The `maxDurationSeconds` you sent; omitted when you sent none. |
| `durationCapped` | boolean | yes | True when `actualDurationSeconds` is shorter than `requestedDurationSeconds`. |
| `billingMode` | string (`reserve`, `metered`) | yes |  |
