Skip to content

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

agentIdstring

The agent's public id, from its Publish page. Required unless you pass getTicket.

baseUrlstringdefault: https://api.voice213.com

The Voice213 API's address.

audioboolean | objectdefault: true in a browser

true uses the browser's microphone and speakers. An object does the same with these settings:

Show audio settings

A microphone's deviceId from navigator.mediaDevices.enumerateDevices(). Default: the system's.

A speaker's deviceId, where the browser lets pages choose (Chrome and Edge).

echoCancellationbooleandefault: true

The browser's echo cancellation. Keep it on, unless every caller wears headphones.

noiseSuppressionbooleandefault: true

The browser's noise suppression.

autoGainControlbooleandefault: true

The browser's automatic microphone level.

echoGatenumber | falsedefault: 0.02

While the agent speaks, microphone sound quieter than this level (0–1) is sent as silence, so the agent never hears itself through the speakers. A caller speaking up still interrupts. It's 0.05 where the browser can't cancel the agent's echo. false turns it off, for headsets and kiosks.

playbackBufferMsnumberdefault: 40

How much of the agent's voice to hold before playing it, against network jitter. The buffer grows by itself after a hiccup.

Where to load the audio processor from, for pages whose Content-Security-Policy blocks blob: scripts. Serve @voice213/agent-client/worklet.js from your site and pass its URL.

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.

getTicket() => Promise<{ token, wsUrl }>

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.

connectTimeoutMsnumberdefault: 50000

How 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.

webSocketWebSocket class

A WebSocket implementation where there's no global one. On Node.js 20, use import WebSocket from "ws".

fetchfunction

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

start()Promise<CallReady>

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.

end()Promise<CallEnded | null>

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.

getSnapshot(), subscribe(listener)AgentCallSnapshot, () => void

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.

PropertyTypeValue
status"idle" | "connecting" | "connected" | "ended" | "error"Where the call is
mode"listening" | "speaking"Whether the agent is talking
isSpeakingbooleanmode === "speaking"
isMutedbooleanWhether the caller is muted
conversationIdstring | nullThe conversation's id, as in your dashboard and your webhooks
transcriptTranscriptMessage[]Completed turns, oldest first: { id, role: "user" | "agent" | "tool", text, ok?, at }
elapsedSecondsnumberSeconds 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

EventPayloadWhen
statusCallStatusThe 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.
messageTranscriptMessageA 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
audioInt16ArrayEach 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.
errorVoice213ErrorThe 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();
}
CodeMeaning
MIC_DENIEDThe visitor refused the microphone
MIC_UNAVAILABLENo microphone, or another app is using it
UNSUPPORTEDThe browser can't make calls here: no microphone access, no AudioWorklet, or the page isn't HTTPS
AUDIOThe audio processor couldn't load. See the workletUrl audio setting
NOT_FOUNDNo live agent with this id, or its link and widget are turned off
ORIGINThis website isn't in the agent's Allowed websites
RATE_LIMITEDToo many calls from this visitor. Try again later
QUOTAThe agent's account has no call minutes left
BUSYEvery line is busy
MODELThe voice model couldn't be reached
UNAVAILABLEVoice213 is down for maintenance
OWNERThe agent's account no longer exists
TICKETVoice213 refused the call for another reason. status has the HTTP code.
ERRORThe call failed for an unexpected reason
NETWORK, CLOSED, TIMEOUTThe connection failed, closed before the call started, or the agent didn't answer in time
CANCELLEDend() was called before the call connected
STATEstart() 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?