Texa DeskDevelopers

Texa AI in your product

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)

JavaScript
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).

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.