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

> Run a realtime voice call through the Phaseo relay with the TypeScript SDK and get the final bill when it ends.

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

**Method**: `client.realtime.connect()` creates the session (`POST /v1/audio/realtime/sessions`), opens the relay WebSocket and resolves once the relay sends `relay.connected`. It sends your API key, so call it on your server only. Node.js 22 or later has a global `WebSocket`; on older runtimes, install the `ws` package or pass your own constructor as `webSocket`.

For a call that runs in the browser, create the session on your server and open the socket in the browser with `connectRealtimeSession()`. It takes the session your server created, including its `client_secret`, which works only for that one session, and never needs the API key. [Browser calls](#browser-calls) shows both halves.

The relay protocol, warnings and reason codes are described in [Realtime voice sessions](https://phaseo.app/docs/v1/guides/realtime-voice.md).

### Example

```typescript theme={null}
import { Phaseo, RealtimeSessionClosedError } from "@phaseo/sdk";

const client = new Phaseo({ apiKey: process.env.PHASEO_API_KEY });

const session = await client.realtime.connect({
  model: "openai/gpt-realtime-2",
  voice: "marin",
  instructions: "Answer in one sentence.",
});

session.onWarning((warning) => console.warn(warning.code, warning.message));
session.on("response.output_audio.delta", (event) => play(event.delta as string));

// PCM16 microphone audio: 24 kHz for OpenAI and xAI, 16 kHz for Google.
session.sendAudio(pcm16Chunk);

try {
  const ended = await session.end({ reason: "caller hung up" });
  console.log(ended.reason, ended.duration_seconds, ended.cost.total_usd, ended.billing_status);
} catch (error) {
  if (error instanceof RealtimeSessionClosedError) {
    // The call is still billed. Fetch the final cost later with error.finalBill.path.
    console.error(error.message);
  } else {
    throw error;
  }
}
```

To read every event instead, iterate the session. Iteration stops when the socket closes.

```typescript theme={null}
for await (const event of session) {
  if (event.type === "session.ended") console.log("bill", event.cost);
}
```

### Browser calls

Never put a Phaseo API key in browser code. On your server, create the session with `client.realtime.createSession()`, which takes the same `model`, `voice`, `instructions`, `provider` and `metadata` options as `connect()`, and return the response to your page:

```typescript theme={null}
// Server route, for example POST /api/voice-session
const info = await client.realtime.createSession({ model: "openai/gpt-realtime-2", voice: "marin" });
return Response.json(info);
```

In the browser, join the session with `connectRealtimeSession()`. It opens the relay with the session's `client_secret` and returns the same session object as `connect()`:

```typescript theme={null}
import { connectRealtimeSession } from "@phaseo/sdk";

const info = await fetch("/api/voice-session", { method: "POST" }).then((response) => response.json());
const session = await connectRealtimeSession(info);
session.sendAudio(pcm16Chunk);
```

`connectRealtimeSession(info, options)` accepts `webSocket` and `connectTimeoutMs` like `connect()`, plus `baseUrl` (default `https://api.phaseo.app/v1`) if you use a regional endpoint.

### Connect options

* `model` (required): a realtime model ID, for example `openai/gpt-realtime-2`.
* `voice`, `instructions`, `provider`, `metadata`: passed to the create-session request.
* `webSocket`: a WebSocket constructor to use instead of the global one.
* `connectTimeoutMs`: how long to wait for `relay.connected`. Default 30 seconds.

### Session

| Member | Description |
| - | - |
| `id`, `info` | The session ID and the create-session response, without `client_secret`. |
| `sendAudio(pcm16)` | Sends a `Uint8Array` or `ArrayBuffer` of PCM16 audio as `client.audio`. The SDK base64-encodes it. |
| `send(event)` | Sends any event as JSON, for provider-specific messages. |
| `on(type, listener)` | Subscribes to one event type, or every event with `"*"`. Returns an unsubscribe function. Provider events arrive unchanged. |
| `onWarning(listener)`, `onEnded(listener)` | Typed helpers for `session.warning` and `session.ended`. |
| `[Symbol.asyncIterator]` | Yields every event from when iteration starts until the socket closes. |
| `end({ reason?, cancelResponse?, timeoutMs? })` | Sends `session.end` and resolves with `session.ended`: `reason`, `initiated_by`, `duration_seconds`, `usage`, `cost`, `billing_status` and, unless settled, `final_bill`. Default timeout 60 seconds. |
| `ended` | A promise for `session.ended` however the call ends, including endings the provider or gateway started. |
| `close()` | Closes the socket without waiting for the bill. The call is still billed. |

### Errors

If the socket closes, or `end()` times out, without `session.ended`, `end()` and `ended` reject with `RealtimeSessionClosedError`. It has `sessionId`, `closeCode`, `closeReason` and `finalBill`, the request that returns the final cost once it is settled: `GET /v1/generations?id=realtime%3A<session_id>`. `realtimeFinalBill(sessionId)` builds the same request.

`connect()` rejects if the relay ends the session before it connects or if `relay.connected` doesn't arrive in time.


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