# Voice in your app

Customers talk to Texa AI from your app or website over a WebSocket, with a single-use token.


Customers talk to Texa AI from your app or website, with the same agent, knowledge and
procedures as on WhatsApp and the phone. Your **server** opens a voice conversation and
asks for a single-use token. Your **app** opens the voice line with that token. The app
never holds a key.

## 1. Your server: a voice conversation and a token

```python
cid = texa.post("/conversations", json={
    "channel": "voice", "customer_ref": "user-1842", "first_name": "Tolu"}).json()["conversation_id"]

session = texa.post(f"/conversations/{cid}/voice-sessions", json={"ttl_s": 120}).json()
# {"conversation_id": "cnv_...", "token": "...", "expires_at": "...",
#  "url": "wss://voice.texadesk.com/v1/voice?token=<token>", "options": {}}
```

Give the app `session["token"]` and `session["url"]`. The token opens one session, once,
before `expires_at` (`ttl_s` is 15 to 900 seconds). Anything about the session (text or
voice mode, the voice) is set here in `options`, never by the app:

| Option | Meaning |
|---|---|
| `mode` | `voice` (default) or `text`: the same line with typed turns |
| `voice` | a voice from Texa Desk's Agent settings, e.g. `spitch:lina` |
| `agent_name`, `agent_gender` | how the agent introduces itself |

## 2. Your app: the line

Open `wss://voice.texadesk.com/v1/voice?token=<token>&codec=<codec>&rate=<rate>`:

- `codec`: `opus` (default; best on mobile networks) or `pcm` (16-bit little-endian mono).
- `rate`: the sample rate, `8000` (default) or `16000`.

The first message tells you exactly what the line carries:

```json
{"type": "ready", "conversation_id": "cnv_...", "rate": 16000,
 "audio": {"codec": "pcm", "sample_rate": 16000, "sample_width": 2},
 "persona": {"name": "Amaka", "language": "en", "gender": "female"}}
```

Then:

| From Texa (text frames) | Do |
|---|---|
| binary frames | play them: Texa AI speaking |
| `{"type": "utterance", "speaker": ..., "text": ...}` | a caption: what was said, by the customer or by Texa AI |
| `{"type": "flush"}` | the customer interrupted: drop the audio you have queued |
| `{"type": "ended", "end_reason": ..., "our_fault": false, "outcome": {...}}` | the call is over; `our_fault: true` means it ended on Texa's side |
| `{"type": "error", "error": ...}` | the line could not open or failed; open a new session |

| From your app | Means |
|---|---|
| binary frames | the microphone, in the codec and rate you asked for |
| `{"type": "hangup"}` | the customer ended the call |
| `{"type": "stats", "playout_underruns": n}` | optional: how often your playback ran dry, which helps Texa find a breaking voice |
| `{"type": "say", "text": "..."}` | in `text` mode, one typed turn |

If `ready` carries a `resume_token`, a line that drops (a tunnel, a lift) can rejoin within
`rejoin_grace_s` seconds by opening `.../v1/voice?resume=<resume_token>`. Texa holds the
call until then.

## A browser sketch (PCM at 16 kHz)

```js
const ws = new WebSocket(`${url.replace('<token>', token)}&codec=pcm&rate=16000`);
ws.binaryType = 'arraybuffer';
const ctx = new AudioContext({ sampleRate: 16000 });
let playAt = 0;

ws.onmessage = ({ data }) => {
  if (typeof data === 'string') {
    const msg = JSON.parse(data);
    if (msg.type === 'flush') playAt = ctx.currentTime;       // drop what is queued
    if (msg.type === 'utterance') showCaption(msg.speaker, msg.text);
    if (msg.type === 'ended') stopMicrophone();
    return;
  }
  const pcm = new Int16Array(data);
  const buf = ctx.createBuffer(1, pcm.length, 16000);
  buf.getChannelData(0).set(Float32Array.from(pcm, s => s / 32768));
  const src = ctx.createBufferSource();
  src.buffer = buf;
  src.connect(ctx.destination);
  playAt = Math.max(playAt, ctx.currentTime);
  src.start(playAt);
  playAt += buf.duration;
};

// microphone -> 16-bit PCM frames of 20 ms (use an AudioWorklet in production)
const mic = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true } });
const node = ctx.createScriptProcessor(320, 1, 1);
node.onaudioprocess = e => {
  const f = e.inputBuffer.getChannelData(0);
  if (ws.readyState === 1) ws.send(Int16Array.from(f, s => Math.max(-1, Math.min(1, s)) * 32767).buffer);
};
ctx.createMediaStreamSource(mic).connect(node);
node.connect(ctx.destination);
```

Turn on echo cancellation, or Texa AI hears itself. Texa's own voice button and mobile
components do all of this for you ([SDKs and widgets](/sdks)).

## After the call

The conversation's transcript is `GET /v1/conversations/{id}/transcript`. Its events
(`call.started`, `call.ended` with the minutes, `conversation.closed`) are on the feed and
in webhooks. Voice minutes are counted against your prepaid credits.
