JavaScript Client
The AgentCall reference: options, methods, events, state and errors
AgentCall is one voice call with a published agent: it opens the microphone, connects, streams audio both ways, and tells you everything that happens through events and a state snapshot. It has no framework and no dependencies.
import { AgentCall } from "@voice213/agent-client";
const call = new AgentCall({ agentId: "k3x9m2p7q4wz" });Options
The agent's public id, from its Publish page. Required unless you pass getTicket.
https://api.voice213.comThe Voice213 API's address.
true in a browsertrue uses the browser's microphone and speakers. An object does the same with these settings:
false turns the browser audio off: you send the caller's audio with sendAudio() and play the audio event yourself. See Custom Audio & Node.js.
Where the call's ticket comes from. By default, the agent's public endpoint (Start a Call). Pass your own to get tickets through your server.
50000How long to wait for the agent to pick up before failing with TIMEOUT. Voice213 tries each voice model in turn before giving up, so keep it generous.
A WebSocket implementation where there's no global one. On Node.js 20, use import WebSocket from "ws".
A fetch implementation (the global one by default).
Callbacks for the events, if you prefer them to on(). They can change between calls with setOptions().
Methods
Starts the call. Call it inside a click handler: browsers only open the microphone and start sound from a user gesture. Resolves when the agent is on the line, with { conversationId, maxSeconds, agent: { name, avatar, language } }. Rejects with a Voice213Error, which error listeners also receive, so you can ignore the promise and use events alone. The one exception is STATE (start() while a call is already running): it only rejects, and the running call carries on. A rejection nobody awaits is never reported as unhandled. Start the same AgentCall again once a call has ended.
Hangs up. The agent's voice stops at once, and the server closes the conversation and answers with its summary { reason, durationSeconds, billedSeconds, conversationId }, which this resolves with. While connecting, it cancels: start() rejects with CANCELLED. The microphone is released right after. When the agent hangs up instead, its goodbye plays out first.
Sends a typed message. The agent answers it like speech, and it appears in the transcript as the caller's turn. Whatever the agent was still saying is dropped. Returns false when the call isn't connected.
Mutes or unmutes the caller. While muted, the agent hears silence: the audio keeps flowing, so the agent still knows the caller has stopped talking.
With audio: false: sends caller audio, 16 kHz 16-bit mono PCM as an Int16Array or ArrayBuffer. See Custom Audio & Node.js.
Switch the microphone or the speaker mid-call. setOutputDevice resolves false where the browser doesn't allow it.
The caller's and the agent's current level, 0 to 1. Read them in a requestAnimationFrame loop to animate a visualiser.
The current spectrum of the caller's or the agent's voice: 128 values, 0–255, for bar visualisers.
Listen to events. on and once return a function that stops listening.
The call's whole state as one immutable object, and a way to be told when it changes. Made for useSyncExternalStore and other state libraries. Both are bound, so you can pass them as they are.
Changes options for the next call. New callbacks take effect at once.
State
Read these at any time. getSnapshot() returns them all as one object, except elapsedSeconds: compute that from startedAt.
| Property | Type | Value |
|---|---|---|
status | "idle" | "connecting" | "connected" | "ended" | "error" | Where the call is |
mode | "listening" | "speaking" | Whether the agent is talking |
isSpeaking | boolean | mode === "speaking" |
isMuted | boolean | Whether the caller is muted |
conversationId | string | null | The conversation's id, as in your dashboard and your webhooks |
transcript | TranscriptMessage[] | Completed turns, oldest first: { id, role: "user" | "agent" | "tool", text, ok?, at } |
elapsedSeconds | number | Seconds since the call connected |
The snapshot also has partial ({ user, agent }, the words being spoken right now), tool (the tool running, if any), agent, maxSeconds, startedAt, error and ended.
Events
| Event | Payload | When |
|---|---|---|
status | CallStatus | The status changes |
ready | { conversationId, maxSeconds, agent } | The agent is on the line |
mode | "listening" | "speaking" | The agent starts or stops talking |
transcript | { role, text, final } | Words as they're spoken. Each event repeats the whole turn so far, so replace the line instead of appending to it. final is true once the turn is complete. |
message | TranscriptMessage | A turn completed, or a tool finished (role: "tool", with ok) |
interrupted | — | The caller cut the agent off. Its queued audio is already dropped. |
tool | { name, status: "running" | "done", ok? } | The agent started or finished one of its tools |
audio | Int16Array | Each chunk of the agent's voice, as 24 kHz 16-bit PCM: a copy, for recording or your own playback |
ended | { reason, durationSeconds, billedSeconds?, conversationId } | The call ended. See end reasons. cancelled means end() was called before it connected. |
error | Voice213Error | The call couldn't start, or stopped on an error |
call.on("transcript", ({ role, text, final }) => {
// One line per turn: replace while it's being spoken, keep it once it's final
const line = live[role] ?? (live[role] = addLine(role));
line.textContent = text;
if (final) delete live[role];
});Errors
Every failure is a Voice213Error with a code, a readable message and, for HTTP refusals, a status:
import { AgentCall, Voice213Error } from "@voice213/agent-client";
try {
await call.start();
} catch (error) {
if (error instanceof Voice213Error && error.code === "MIC_DENIED") showMicrophoneHelp();
}| Code | Meaning |
|---|---|
MIC_DENIED | The visitor refused the microphone |
MIC_UNAVAILABLE | No microphone, or another app is using it |
UNSUPPORTED | The browser can't make calls here: no microphone access, no AudioWorklet, or the page isn't HTTPS |
AUDIO | The audio processor couldn't load. See the workletUrl audio setting |
NOT_FOUND | No live agent with this id, or its link and widget are turned off |
ORIGIN | This website isn't in the agent's Allowed websites |
RATE_LIMITED | Too many calls from this visitor. Try again later |
QUOTA | The agent's account has no call minutes left |
BUSY | Every line is busy |
MODEL | The voice model couldn't be reached |
UNAVAILABLE | Voice213 is down for maintenance |
OWNER | The agent's account no longer exists |
TICKET | Voice213 refused the call for another reason. status has the HTTP code. |
ERROR | The call failed for an unexpected reason |
NETWORK, CLOSED, TIMEOUT | The connection failed, closed before the call started, or the agent didn't answer in time |
CANCELLED | end() was called before the call connected |
STATE | start() was called while a call is already running |
See Errors & Troubleshooting for what to do about each one.
A complete example
<button id="talk">Talk to us</button>
<button id="mute" hidden>Mute</button>
<span id="status"></span>
<canvas id="meter" width="120" height="120"></canvas>
<ol id="lines"></ol>
<script type="module">
import { AgentCall } from "https://cdn.jsdelivr.net/npm/@voice213/agent-client@0.2.0/+esm";
const call = new AgentCall({ agentId: "k3x9m2p7q4wz" });
const $ = (id) => document.getElementById(id);
const live = {};
call.on("status", (status) => {
$("status").textContent = status;
$("talk").textContent = status === "connected" ? "Hang up" : status === "connecting" ? "Connecting…" : "Talk to us";
$("mute").hidden = status !== "connected";
if (status === "connected") requestAnimationFrame(draw);
});
call.on("transcript", ({ role, text, final }) => {
const line = live[role] ?? (live[role] = $("lines").appendChild(Object.assign(document.createElement("li"), { dir: "auto" })));
line.textContent = `${role === "user" ? "You" : "Agent"}: ${text}`;
if (final) delete live[role];
});
call.on("error", (error) => ($("status").textContent = error.message));
$("talk").onclick = () => (call.status === "connected" ? call.end() : call.start());
$("mute").onclick = () => ($("mute").textContent = call.toggleMute() ? "Unmute" : "Mute");
// An orb that follows whoever is talking
function draw() {
const level = call.isSpeaking ? call.getOutputVolume() : call.getInputVolume();
const g = $("meter").getContext("2d");
g.clearRect(0, 0, 120, 120);
g.beginPath();
g.arc(60, 60, 30 + level * 28, 0, Math.PI * 2);
g.fillStyle = call.isSpeaking ? "#F76B1C" : "#9CA3AF";
g.fill();
if (call.status === "connected") requestAnimationFrame(draw);
}
</script>Helpers
import { fetchAgent, requestCallTicket, VERSION } from "@voice213/agent-client";
const agent = await fetchAgent("k3x9m2p7q4wz");
// { name, description, avatar: { from, to, url }, accent, language, greeting }fetchAgent reads a published agent's public profile, to build your button with its picture, colours and language before any call. See Get Public Agent. requestCallTicket asks for a call ticket yourself. You rarely need it, because AgentCall does it.
Was this page helpful?