# A voice call is one WebSocket Tileward PersonaPlex 7B listens and talks at the same time: open a socket with your API key, send your microphone, and hear it answer. This page gives the address, the key, the audio frames, the close codes and how a call is billed, and how Tileward VoiceChat 11B answers from your Tileward Context store during a call. Page: https://tileward.com/docs/voice/ ## Try it in the console The console's [call page](https://app.tileward.com/call) puts you on a call from the browser: pick a voice, say who it is, and talk. It is the same endpoint and the same meter as the API. ## Connect | | Value | |---|---| | Address | `wss://api.tileward.com/v1/voice` | | Key | `Authorization: Bearer tw_live_...`, or the same key in `x-api-key` | | `voice` | One of 18 voices: `NATF0` to `NATF3`, `NATM0` to `NATM3`, `VARF0` to `VARF4`, `VARM0` to `VARM4`. Optional, `NATF2` if you leave it out. | | `prompt` | Who the voice is, up to 2,000 characters. Optional. | - `voice` and `prompt` go in the query string. - A browser cannot set a header on a WebSocket, so open the socket from your own server and keep the key there. - A key holds one call at a time. - Get a key from the console under [Settings → API keys](https://app.tileward.com/settings/keys). ``` # pip install "websockets>=14" import asyncio, os from urllib.parse import urlencode import websockets query = urlencode({ "voice": "NATF2", "prompt": "You work for Acme Dental which is a dental clinic and your name is Sam.", }) url = f"wss://api.tileward.com/v1/voice?{query}" headers = {"Authorization": f"Bearer {os.environ['TILEWARD_API_KEY']}"} async def talk(ws): async for page in microphone(): # yours: Ogg/Opus pages, 24 kHz mono await ws.send(b"\x01" + page) async def main(): async with websockets.connect(url, additional_headers=headers) as ws: sending = None try: async for frame in ws: if isinstance(frame, str): print(frame) # JSON: an error, or the bill when the call closes elif frame[0] == 0: sending = asyncio.create_task(talk(ws)) # the call is live: start sending elif frame[0] == 1: play(frame[1:]) # yours: the voice, Ogg/Opus, 24 kHz mono elif frame[0] == 2: print(frame[1:].decode(), end="") # the words it is saying except websockets.ConnectionClosed: pass if sending: sending.cancel() print("closed with code", ws.close_code) # see the close codes below asyncio.run(main()) ``` `microphone()` and `play()` are yours to supply: an Opus encoder and decoder at 24 kHz mono. The model answers what it hears, so send your microphone from the moment the call is live: the call bills from then whether or not audio is flowing. ## On the wire Audio is binary; everything else is JSON from Tileward. The first byte of a binary message says what it carries. | Message | Direction | What it is | |---|---|---| | Binary, first byte `0` | To you | The call is live. Billing starts here. | | Binary, first byte `1` | Both ways | Ogg/Opus audio, 24 kHz mono, after the first byte: your microphone going up, the voice coming back. | | Binary, first byte `2` | To you | A piece of the text the model is saying. | | `{"type": "error", "code": "...", "message": "..."}` | To you | Why a call was refused or ended. The socket closes after it. | | `{"type": "closed", "reason": "...", "billed_seconds": 30.0, "cost_micros": "40000.0000"}` | To you | The bill. `reason` is `client`, `host`, `balance` or `limit`, and `cost_micros` is in millionths of a dollar. | The model reads the prompt before the call goes live. That takes a few seconds for a short prompt and longer for a long one, and it is not billed. ## Close codes | Code | Meaning | |---|---| | `1000` | The call ended normally. | | `4400` | Bad request: an unknown `voice`, a `prompt` over 2,000 characters, or a `seed`, which a voice call does not take. | | `4401` | The key is missing or invalid. | | `4402` | The balance was empty at the start, or ran out during the call. | | `4403` | The key has a governance policy bound to it. A voice call takes a key without one. | | `4429` | Rate limit, or this key already has a call in progress. | | `4503` | No line is free right now. When the error carries `retry_after_s`, a line is starting: try again after that many seconds. Nothing was billed. | ## Billing - A call is billed by the minute of conversation from your account credit, counted in 80 ms steps from the moment it goes live, with a ten-second minimum. - Silence bills the same as speech, and a call that never goes live bills nothing. - Each voice model's rate is in the [model library](https://tileward.com/model-library/). ## Tileward VoiceChat 11B: a call that looks things up Tileward VoiceChat 11B answers from your own Tileward Context store while the call is running. It is ready to serve on request: [talk to an engineer](https://calendar.app.google/hg2DzFTxKJgwFWm19) to start. A call then connects with the same key, the same close codes and the same one call per key. | | Value | |---|---| | Address | `wss://api.tileward.com/v1/voice/chat` | | `prompt` | Who the voice is, up to 2,000 characters. Optional. | | `context` | The store it answers from: `knowledge` for your uploaded documents, or the name of one of your Context conversations. Optional. | | You send | Binary PCM audio: signed 16-bit little-endian, 16 kHz, mono. | | You receive | Binary PCM audio: signed 16-bit little-endian, 22,050 Hz, mono, 80 ms a message. | | Text message | What it is | |---|---| | `{"type": "ready"}` | The call is live. Billing starts here. | | `{"type": "transcript.user", "delta": "..."}` | What the caller said. | | `{"type": "transcript.agent", "delta": "..."}` | What the voice is saying. | | `{"type": "lookup", "found": true}` | A question was looked up in your store, and whether it found an answer. | | `{"type": "closed", ...}` | The bill, as above, with `lookups` and `found` counts for the call. | | `{"type": "error", ...}` | Why a call was refused or ended. | To end the call, send `{"type": "session.close"}` or close the socket. The call is billed by the minute like the one above, plus the tokens its lookups use, and each lookup counts as one Context recall. ## Related - [Model library](https://tileward.com/model-library/): both voice models with their price and licence - [The PersonaPlex write-up](https://tileward.com/research/a-smaller-voice-model/): how the PersonaPlex build was measured - [Endpoints](https://tileward.com/docs/#endpoints): which address wants which credential, for chat, guard and Context - [Connect a client](https://tileward.com/docs/): a recipe for each client - [Call page](https://app.tileward.com/call): a voice call from the browser