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
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) orpcm(16-bit little-endian mono).rate: the sample rate,8000(default) or16000.
The first message tells you exactly what the line carries:
{"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)
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.