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

> ## Agent Instructions
> Phaseo Gateway base URL: https://api.phaseo.app/v1. Send `Authorization: Bearer $PHASEO_API_KEY` with every request.
> Model IDs look like `provider/model`, for example `openai/gpt-6-astra`. Do not guess IDs: list them with `GET /v1/models` or check https://phaseo.app/models. `phaseo/free` routes to an available free model and needs no wallet balance.
> For new text integrations prefer `POST /v1/responses`. `POST /v1/chat/completions` (OpenAI format) and `POST /v1/messages` (Anthropic format) suit existing code.
> The full API contract is https://phaseo.app/docs/openapi/v1/openapi.yaml. Official SDKs: `@phaseo/sdk` (TypeScript) and `phaseo` (Python), plus Go, C#, PHP, Ruby and Rust.
> Search these docs over MCP at https://phaseo.app/docs/mcp, and install Phaseo skills with `npx skills add https://phaseo.app/docs`. If a page is wrong or out of date, report it with the MCP `submit_feedback` tool.
> Manage Phaseo accounts with the `phaseo` CLI (`npm install -g @phaseo/cli`, then `phaseo login` or `phaseo login --device-code`), the `/v1` Management API or the MCP server at `https://mcp.phaseo.app/mcp?access=manage`. Do not send users to the dashboard for account tasks. A person is still needed for sign-up, approving a sign-in, paying the first top-up on the checkout link that `phaseo credits add` returns when no card is saved, confirming each saved-card top-up from the emailed link, and account security (password, MFA, passkeys). Create API keys with `phaseo keys create --name <name> --json`; never guess one. Credit top-ups need `phaseo login --scopes billing:write` first. Task map: https://phaseo.app/docs/v1/developers/manage-from-an-agent.md.

# Realtime voice sessions

> Run a realtime voice call through the Phaseo relay, end it cleanly, and get the final bill when the call ends.

> [!NOTE]
>
> Realtime voice is in beta and access is gated per workspace.

A realtime session is a voice call between your client and a provider's realtime model, relayed by Phaseo so it can be metered and billed. The relay forwards the provider's own events unchanged and adds three of its own: `session.end`, `session.warning` and `session.ended`.

## Start a session

Create the session from your server:

```bash theme={null}
curl https://api.phaseo.app/v1/audio/realtime/sessions \
  -H "Authorization: Bearer $PHASEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "openai/gpt-realtime-2", "voice": "marin"}'
```

The response includes `client_secret`, `connect.url`, `max_duration_seconds` and `idle_timeout_seconds` (always `null` for API sessions). Open a WebSocket to `connect.url` on `api.phaseo.app` with the WebSocket subprotocols `statsync-realtime` and `rtsec.<client_secret>`. The relay selects `statsync-realtime`; browsers and most WebSocket libraries fail the handshake if you offer only `rtsec.<client_secret>`, because no protocol is selected. The relay sends `relay.connected` once the provider is ready, then accepts microphone audio as `{"type": "client.audio", "audio": "<base64 PCM16>"}`: 24 kHz for OpenAI, xAI and ElevenLabs, 16 kHz for Google and Mistral.

Phaseo holds \$5 of credit when the session starts and extends the hold as the call runs.

The [TypeScript](https://phaseo.app/docs/v1/sdk-reference/typescript/audio-realtime.md) and [Python](https://phaseo.app/docs/v1/sdk-reference/python/audio-realtime.md) SDKs do all of this for you: `client.realtime.connect()` creates the session and opens the socket, and `session.end()` returns `session.ended`. The other SDKs have the same small client; each package README has an example.

| SDK | Connect | WebSocket library |
| - | - | - |
| Go | `realtime.Connect(ctx, client, realtime.Options{...})` | `github.com/coder/websocket`, in the separate `realtime` package |
| Java | `client.realtime.connect(Map.of("model", ...))` | JDK `java.net.http.WebSocket` |
| C# | `await client.Realtime.ConnectAsync(new RealtimeConnectOptions { ... })` | `System.Net.WebSockets.ClientWebSocket` |
| PHP | `$client->realtime()->connect(["model" => ...])` | `phrity/websocket` (optional, `composer require phrity/websocket`) |
| Ruby | `client.realtime.connect(model: ...)` | `websocket` gem (optional) |
| Rust | `client.realtime().connect(RealtimeOptions::new(...))` | `tungstenite`, behind the `realtime` feature |

The C++ SDK has no realtime client.

### Google Live connections

Keep streaming microphone audio for the whole call, including silence. Google closes a connection that receives no audio for about 150 seconds (close code `1008`), and the call then ends with `provider_closed`. The relay sends a `provider_closing` warning with `threshold: "input_idle"` after 2 minutes without audio. Silence between turns is not part of any turn, so it isn't billed.

Google also replaces each connection after about 10 minutes. The relay moves the call to a new connection with Google's session resumption, keeps your socket open and sends `{"type": "relay.provider_reconnected", "reconnects": 1}` when the switch is done. Audio you send during the switch (usually under a second) is held and passed on. The resumed connection reprocesses the conversation so far, and Google bills that again as input tokens on the next turn, so a long call costs a little more per turn after each switch. All of it is included in `usage` and the bill. The relay only warns with `provider_closing` if it can't resume, and the call then ends when Google closes the connection.

### GPT Live sessions

GPT Live (`openai/gpt-live-1`) handles the voice and hands reasoning to a Responses backend model. Create it with `POST /v1/live/sessions`:

```bash theme={null}
curl https://api.phaseo.app/v1/live/sessions \
  -H "Authorization: Bearer $PHASEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-live-1",
    "provider": "openai",
    "voice": "marin",
    "backend_model": "openai/gpt-5.6-luna",
    "backend_settings": {
      "tools": [{ "type": "function", "name": "get_time", "description": "Returns the current UTC time.", "parameters": { "type": "object", "properties": {} } }]
    }
  }'
```

`voice` is one of `alloy`, `ash`, `ballad`, `beacon`, `bossa`, `cedar`, `cinder`, `coral`, `delta`, `echo`, `gleam`, `marin`, `meridian`, `quartz`, `ripple`, `sage`, `shimmer`, `stone`, `tempo`, `verse`, `vesper` or `willow`. `backend_model` is `openai/gpt-5.6-luna` (default) or `openai/gpt-5.6-terra`. Connect and stream 24 kHz audio the same way as other sessions.

Function tools run on your client. The backend's call arrives as a `response.event` whose `event.item.type` is `function_call`. Its name and `arguments` come from the model, and what the caller says can steer them, so treat them as untrusted input:

1. Run only tools you declared, looked up by `name`, and refuse any other name.
2. Parse `arguments` as JSON and validate it against the tool's `parameters` schema before using it.
3. Check that the signed-in user is allowed to do what the call asks, and confirm with them before anything that changes data, spends money or contacts someone.

Then run the tool, send the result and ask for the follow-up reply. If a call fails these checks, send an `output` that says why instead of running it:

```json theme={null}
{ "type": "response.item.create", "item": { "type": "function_call_output", "call_id": "call_63hZ...", "output": "{\"utc\":\"2026-10-22T09:42:48Z\"}" } }
{ "type": "response.create" }
```

The relay accepts a `function_call_output` only for a call the backend made, once per call and up to 64 KB, and `response.create` only after at least one output. Anything else gets `{"type": "relay.client_event_rejected", "reason": "..."}` and the call continues. Phaseo never runs function tools itself.

A GPT Live call is billed per second of voice (\$0.05 per minute, from GPT Live's own `usage.seconds`) plus the backend model's tokens for every response, including the replies after tool results. GPT Live expires a session that receives no audio for about 30 seconds, which ends the call with `provider_idle_timeout`. Keep streaming microphone audio, silence included.

### ElevenLabs Realtime sessions

ElevenLabs Realtime comes in two models: `eleven-labs/eleven-realtime-v1-mini`, the faster and cheaper default, and `eleven-labs/eleven-realtime-v1-max`, which reasons before it answers and suits complex instructions and multi-step tool use. Both use the same create request and relay as OpenAI, with 24 kHz audio. Phaseo connects to ElevenLabs with its own key; your client only ever sees the relay.

ElevenLabs bills a call for every second it's connected, plus the language-model tokens it uses, and it doesn't report usage during the call. The relay meters connected time while the call runs, so holds and the overdraft cap work as for other providers. When the call ends, Phaseo reads ElevenLabs' bill for the conversation and charges the larger of that and its own list-price total. That usually takes a few seconds. If it takes longer, `session.ended` arrives with `billing_status: "pending"` and a `final_bill` request, and the bill is settled as soon as ElevenLabs reports it, normally within a few minutes. Your holds stay in place until then. If ElevenLabs hasn't reported the bill after 24 hours, the session moves to billing review rather than being charged for connected time alone.

### Streaming transcription

Mistral's Voxtral Realtime models transcribe live audio as it arrives: `mistral/voxtral-mini-transcribe-realtime-3` and `mistral/voxtral-mini-transcribe-realtime`. They don't speak back, so create the session without `voice` or `instructions`:

```bash theme={null}
curl https://api.phaseo.app/v1/audio/realtime/sessions \
  -H "Authorization: Bearer $PHASEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "mistral/voxtral-mini-transcribe-realtime-3"}'
```

Connect the same way and stream 16 kHz PCM16 as `client.audio`. Mistral's events are forwarded unchanged: `session.created`, then `transcription.text.delta` with each new piece of text, and `transcription.language` and `transcription.segment` where Mistral sends them. To change the transcription settings, send `{"type": "session.update", "session": {...}}` with any of `language`, `target_streaming_delay_ms` (80 to 1,200, or 2,400), `context_bias`, `diarize` and `turn_detection`. Other fields, including the audio format, are rejected with `relay.client_event_rejected`. Settings are fixed once audio starts, so send `session.update` before the first `client.audio`; later updates are rejected the same way. The older `mistral/voxtral-mini-transcribe-realtime` accepts only `target_streaming_delay_ms`.

When you send `session.end`, the relay ends Mistral's audio stream and waits up to 10 seconds for `transcription.done`, which carries the full transcript, before it settles and sends `session.ended`. Use the audio you have already sent; nothing is discarded.

Streaming transcription is billed per minute of audio you send (\$0.008 per minute for Voxtral Mini Transcribe Realtime 3), measured by the relay. Mistral's own count in `transcription.done` is the minimum charged. For a recording you already have, use [audio transcriptions](https://phaseo.app/docs/v1/api-reference/endpoint/audio-transcriptions.md) instead.

## End a session

Send `session.end` when the caller hangs up:

```json theme={null}
{ "type": "session.end", "reason": "caller hung up" }
```

| Field | Type | Default | Meaning |
| - | - | - | - |
| `reason` | string | none | Your own note, up to 120 printable characters, echoed back as `client_reason`. |
| `cancel_response` | boolean | `true` | `true` cancels a reply that is still playing. `false` lets it finish, for up to 20 seconds. |

The relay then:

1. stops accepting audio;
2. discards microphone audio the provider has not committed to a turn;
3. cancels the reply in progress, or lets it finish, and waits up to 10 seconds for its usage;
4. for GPT Live, sends the provider `session.close` and waits for `session.closed`;
5. settles the bill;
6. sends `session.ended`;
7. closes the socket with code `1000`.

Closing the socket without `session.end` also ends the call and bills it the same way, but your client won't receive `session.ended`.

## session.ended

The relay sends `session.ended` on every ending: one you requested, one the provider started and one the gateway started. It's sent while your socket is still open, just before the relay closes it.

```json theme={null}
{
  "type": "session.ended",
  "session_id": "rt_01k8...",
  "reason": "client_requested",
  "initiated_by": "client",
  "client_reason": "caller hung up",
  "duration_seconds": 184.2,
  "usage": { "input_audio_tokens": 1840, "output_audio_tokens": 3120, "input_text_tokens": 412 },
  "cost": {
    "total_usd": 0.231,
    "total_nanos": 231000000,
    "estimated": false,
    "estimate_basis": null,
    "debt_nanos": 0,
    "uncollected_nanos": 0,
    "currency": "USD"
  },
  "billing_status": "settled"
}
```

| Field | Meaning |
| - | - |
| `reason` | A stable reason code. See [reason codes](#reason-codes). |
| `initiated_by` | `client`, `gateway` or `provider`. |
| `duration_seconds` | Time since the relay connected. |
| `usage` | The billed meters: token counts by modality and, for xAI and GPT Live, `audio_seconds`. |
| `cost.estimated` | `true` when part of the usage was estimated from metered audio because the provider's usage for an interrupted turn never arrived. `estimate_basis` says why. |
| `cost.debt_nanos` | The part of the cost charged as overdraft beyond your holds and free balance. |
| `cost.uncollected_nanos` | Cost beyond the overdraft cap that wasn't charged. |
| `provider_close_code` | The provider's WebSocket close code, when the provider ended the call. |
| `billing_status` | `settled`, `pending` or `unresolved`. |

If settlement is still being retried, `billing_status` is `pending` and `cost` shows the running estimate. If the session needs a billing review, it's `unresolved`. In both cases `final_bill` gives the request to fetch the final cost once it's settled:

```json theme={null}
"final_bill": { "method": "GET", "path": "/v1/generations?id=realtime%3Art_01k8..." }
```

### Reason codes

| `reason` | `initiated_by` | When |
| - | - | - |
| `client_requested` | client | You sent `session.end`. |
| `client_disconnected` | client | Your socket closed without `session.end`. |
| `provider_closed` | provider | The provider closed its connection, for example Google after about 150 seconds without audio (code `1008`), or after a `goAway` the relay couldn't resume from. |
| `provider_idle_timeout` | provider | GPT Live expired the session because it received no audio for about 30 seconds. |
| `provider_error` | provider | The provider connection failed or reported an error. |
| `max_duration_reached` | gateway | The provider's session limit was reached. |
| `insufficient_funds` | gateway | The overdraft cap was reached. |
| `key_revoked` | gateway | The API key was revoked or deleted during the call. |
| `key_expired` | gateway | The API key expired during the call. |
| `key_blocked` | gateway | The API key was paused during the call. |
| `workspace_disabled` | gateway | The workspace no longer exists. |
| `policy_blocked` | gateway | A workspace policy change blocked the model, or removed the provider from the allowed list, and was still in place 30 seconds after the `policy_blocked` warning. |
| `input_rejected` | gateway | The relay rejected client audio: too large, faster than real time, or the provider fell behind. |
| `session_reconciled` | gateway | The relay lost its state and the session was closed by reconciliation. |
| `gateway_error` | gateway | Phaseo couldn't process provider events or record usage. |
| `idle_timeout` | gateway | Playground only: five minutes without audio. |
| `spend_limit_reached` | gateway | Playground only: a key spend limit or workspace budget was reached. |

New codes may be added, so treat an unknown `reason` like `gateway_error`.

## session.warning

Warnings tell you what's coming without ending the call. Each code is sent at most once per threshold.

```json theme={null}
{ "type": "session.warning", "code": "max_duration_approaching", "message": "...", "seconds_remaining": 120, "expires_at": "2026-10-22T11:00:00.000Z" }
```

| `code` | Extra fields | When |
| - | - | - |
| `max_duration_approaching` | `seconds_remaining`, `expires_at` | 2 minutes and 30 seconds before the provider's session limit. |
| `provider_closing` | `seconds_remaining`, and `threshold: "input_idle"` for the idle case | Google will close the connection: after a `goAway` the relay couldn't resume from, or (`input_idle`) because no audio has arrived for 2 minutes. |
| `funds_low` | `remaining_usd` | The credit hold couldn't be extended. The call continues on your free balance. |
| `overdraft_in_use` | `remaining_usd`, `overdraft_used_usd` | Your free balance is used up and the call is running on overdraft. Sent again when less than \$0.25 of the cap is left. |
| `spend_limit_exceeded` | `limit` (`key` or `workspace_budget`) | A key spend limit or workspace budget was reached. The call continues; new requests are refused. |
| `policy_blocked` | `status`, plus `seconds_remaining` and `ends_at` when `status` is `ending` | A workspace policy now blocks the call, or its provider was removed from the allowed list. The call ends after 30 seconds unless the change is undone, in which case a second warning arrives with `status: "cleared"`. |

`remaining_usd` is what the call can still spend before it ends, including the overdraft.

## Limits

API sessions are never ended for being idle, and spend limits only warn. A call ends on its own only when one of these happens:

| Limit | Value |
| - | - |
| OpenAI Realtime | 60 minutes |
| xAI | 120 minutes |
| Google Live | 60 minutes, across the connections the relay resumes. The call also ends if your client stops sending audio for about 150 seconds. |
| GPT Live | 25 minutes |
| ElevenLabs Realtime | 30 minutes |
| Mistral streaming transcription | 60 minutes |
| Overdraft cap | \$1 beyond your holds and free balance |
| Lost access | The key is revoked, expired or paused, or the workspace is removed. Detected within about a minute, then the call ends without a grace period. |
| Policy change | 30 seconds after a `policy_blocked` warning, if the policy still blocks the call |

Session limits count from when the relay connects, not from when the session was created.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.