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

# Realtime: web and mobile

> Put an agent in a web page or an app over a WebSocket. No SIP trunk, no phone number.

A phone call is one way to reach an agent. Realtime is the other: your user
talks to the same agent from a browser tab, a mobile app or a kiosk, over a
WebSocket. The engines, prompts, knowledge bases and appointment booking are
the ones your phone calls use — only the line is different.

Two steps, and the split is the security model:

1. **Your server** calls `POST /v1/realtime/sessions` with your API key and gets
   a `client_secret`.
2. **Your client** opens `wss://api.phone.wixzel.com/v1/realtime?client_secret=…`
   and streams audio.

The secret works **once**, for **one minute**, for **one agent**. Your API key
never leaves your server.

## 1. Create a session (server)

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.phone.wixzel.com/v1/realtime/sessions \
    -H "Authorization: Bearer $WIXZEL_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "agent_id": "6a96a3ead6e886d42462dd3e",
      "allowed_origins": ["https://app.example.com"],
      "metadata": { "user_id": "u_812" }
    }'
  ```

  ```ts TypeScript theme={null}
  const session = await wixzel.realtime.createSession({
    agent_id: '6a96a3ead6e886d42462dd3e',
    allowed_origins: ['https://app.example.com'],
    metadata: { user_id: 'u_812' },
  });
  // send { url: session.url, client_secret: session.client_secret } to your client
  ```

  ```dart Dart theme={null}
  final session = await client.realtime.createSession(CreateRealtimeSession(
    agentId: '6a96a3ead6e886d42462dd3e',
    metadata: {'user_id': 'u_812'},
  ));
  ```
</CodeGroup>

```json Response theme={null}
{
  "id": "rt-5b0c7c0e-9d1f-4c52-8a9e-2f0d3a4b6c7d",
  "object": "realtime_session",
  "client_secret": { "value": "rt_secret_…", "expires_at": "2026-09-12T10:01:00.000Z" },
  "url": "wss://api.phone.wixzel.com/v1/realtime",
  "agent_id": "6a96a3ead6e886d42462dd3e",
  "engine": "gemini_live",
  "max_duration_seconds": 600,
  "audio_format": "mulaw_8000"
}
```

| Field                  |                                                                                                                                                                           |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_id`             | Required. One of your agents.                                                                                                                                             |
| `lead_id`              | Optional. Attributes the session to a lead: `{{name}}` merge fields resolve from it and appointment booking uses its phone. Without one, merge fields resolve to nothing. |
| `metadata`             | Optional. Your own ids, echoed on the resulting call and its `callCompleted` webhook. Never sent to the model.                                                            |
| `max_duration_seconds` | Optional. 10–3600, default 600. The server ends the session at this point.                                                                                                |
| `allowed_origins`      | Optional, recommended for browsers. The socket is refused unless the page's `Origin` matches, so a secret copied out of your page cannot be used from another site.       |

Needs the `calls:write` scope. **Nothing is charged here** — a user who opens
your page and then denies the microphone costs you nothing.

## 2. Connect (client)

### In a browser

The TypeScript SDK ships a browser client that handles the microphone,
encoding, playback and barge-in:

```ts theme={null}
import { RealtimeSession } from 'wixzel-phone/realtime';

// `session` is what your server returned above
const call = await RealtimeSession.connect(session, {
  onTranscript: ({ role, text }) => render(role, text),
  onAgentSpeaking: (speaking) => setSpeaking(speaking),
  onError: ({ code, message }) => showError(code, message),
  onEnd: (reason) => showEnded(reason),
});

// later
call.setMuted(true);
await call.end();
```

It needs a secure page (`https://`, or `localhost`) and asks for the
microphone when `connect` is called. To ask **before** you mint a session,
call `requestMicrophone()` first and pass the stream to `session.start()`.

### In Flutter or Dart

The Dart SDK has the protocol client. Microphone and speaker access are your
app's — use any recording plugin that gives you 16-bit PCM at 8 kHz, and any
PCM player:

```dart theme={null}
import 'package:wixzel_phone/wixzel_phone.dart';

final conn = RealtimeConnection.forSession(session);
conn.events.listen((e) {
  switch (e) {
    case RealtimeAudio(:final mulaw): player.add(mulawToPcm16(mulaw));
    case RealtimeAudioClear(): player.clear(); // the user interrupted
    case RealtimeTranscript(:final role, :final text): print('$role: $text');
    case RealtimeError(:final code, :final message): print('$code: $message');
    case RealtimeEnded(:final reason): print('ended: $reason');
    default:
  }
});
mic.listen((Int16List pcm) => conn.sendAudio(pcm16ToMulaw(pcm)));
// later
await conn.end();
```

<Note>
  Turn on your platform's echo cancellation. Without it the agent hears itself
  through the speaker and interrupts itself.
</Note>

### Anything else

The protocol is plain JSON over a WebSocket, below.

## The protocol

Every frame is a JSON text message with a `type`. Audio is **base64 G.711
µ-law, 8 kHz, mono** in both directions — exactly what a phone line carries.
Send it in 20 ms frames (160 bytes).

**From the server**

| `type`            | Fields                                                                                | Meaning                                                                           |
| ----------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `session.started` | `session_id`, `call_id`, `agent_id`, `engine`, `max_duration_seconds`, `audio_format` | Admitted; the agent is connecting and will greet the user.                        |
| `audio`           | `audio`                                                                               | Agent speech. Play it in order.                                                   |
| `audio.clear`     |                                                                                       | The user interrupted. Drop everything you have queued.                            |
| `transcript`      | `role` (`user` or `assistant`), `text`                                                | A line of the conversation.                                                       |
| `session.warning` | `code`, `seconds_remaining`                                                           | `low_balance`: the account is about to run out of credit.                         |
| `error`           | `code`, `message`                                                                     | Something went wrong. Before `session.started`, it means the session was refused. |
| `session.ended`   | `reason`, `duration_seconds`                                                          | Over. The socket closes next.                                                     |

**From the client**

| `type`        | Fields  | Meaning           |
| ------------- | ------- | ----------------- |
| `audio`       | `audio` | Microphone audio. |
| `session.end` |         | Hang up.          |

While the agent is saying its opening line, keep sending audio but send
silence (`0xFF` bytes): the agent then cannot be interrupted by room noise
before the user has heard anything. The browser client does this for you.

### Why a session ends

| `reason`               |                                                           |
| ---------------------- | --------------------------------------------------------- |
| `client`               | You sent `session.end`.                                   |
| `agent`                | The agent ended the conversation — the user said goodbye. |
| `max_duration`         | It reached `max_duration_seconds`.                        |
| `idle`                 | Nobody spoke for two minutes.                             |
| `insufficient_credits` | The account ran out of credit mid-session.                |
| `error`                | The voice engine failed.                                  |
| `disconnected`         | The socket dropped.                                       |

### Refusals

A browser cannot read the HTTP status of a refused WebSocket, so refusals
arrive as an `error` frame followed by a close with one of these codes:

| Close | `code`                                     | Fix                                                                                                     |
| ----- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| 4401  | `invalid_client_secret`, `invalid_api_key` | The secret is missing, expired or already used — mint a new one. Or the key that minted it was revoked. |
| 4402  | `insufficient_credits`                     | Top up.                                                                                                 |
| 4403  | `origin_not_allowed`                       | The page's origin is not in `allowed_origins`.                                                          |
| 4404  | `agent_not_found`                          | The agent was deleted after minting.                                                                    |
| 4409  | `concurrency_limit`                        | The account is at its concurrent-call limit.                                                            |
| 4429  | `spend_limit`                              | The API key reached its spend limit.                                                                    |
| 4503  | `engine_unavailable`, `server_busy`, …     | Try again shortly.                                                                                      |

## Billing and records

A realtime session **is a call** to everything but the phone network:

* It is billed at the **same per-minute price** as a phone call on the same
  engine, per second, from credit. See [pricing](https://phone.wixzel.com/pricing).
* It counts toward your **concurrent-call limit** (5 by default) and the API
  key's **spend limit**.
* It appears in `GET /v1/calls` with `"channel": "web"` (filter with
  `?channel=web`), inbound, with no `from` or `to`, and with its transcript.
* It ends with a `callCompleted` [webhook](/webhooks) carrying
  `"provider": "web"` and your `metadata`.

Human transfer is not available: there is no second line to hand the user to.
Web sessions are not recorded.
