@zavudev/voice
v0.1.1
Published
Talk to a Zavu voice agent from the browser
Maintainers
Readme
@zavudev/voice
Talk to a Zavu voice agent from the browser. No phone number, no phone call.
npm install @zavudev/voiceHow it fits together
Two halves, and the split is the point: your API key never reaches the browser.
// 1. Your server. Holds the API key, decides who is allowed to talk.
import Zavu from "@zavudev/sdk";
const zavu = new Zavu({ apiKey: process.env.ZAVU_API_KEY });
export async function POST() {
const session = await zavu.voice.webSessions.create({
agentId: process.env.ZAVU_AGENT_ID,
maxSeconds: 300,
});
return Response.json({ token: session.token });
}// 2. Your visitor's browser. Only ever sees a token.
import { ZavuVoice, formatMs } from "@zavudev/voice";
const { token } = await fetch("/api/voice-session", { method: "POST" }).then((r) =>
r.json()
);
const call = await ZavuVoice.connect({ token });
call.on("state", (state) => console.log(state)); // connecting → live → ended
call.on("turn", (turn) => render(turn.role, turn.text));
call.on("latency", (s) => console.log(`replied in ${formatMs(s.ms)}`));
call.on("error", (e) => console.error(e.code, e.message));
// later
call.hangup();The token is single-use, expires in two minutes, and the cost of the session was already held against your balance when your server minted it. A visitor who loads the page and never clicks costs nothing.
What the events mean
| Event | Fires when |
|---|---|
| state | connecting on dial, live when the call is actually answered — not when the socket opens — then ended or error. |
| turn | A speaker said something. streaming: true means it is still being appended to. |
| latency | An agent reply closed a user turn. See below. |
| error | Microphone denied, session rejected, rate limited, or the call dropped. |
About the latency number
It is the gap between the user's turn being finalized and the agent's first words arriving, measured in the browser against one clock.
There is no per-stage breakdown and there will not be one. Speech
recognition, the model and speech synthesis run inside one managed pipeline that
reports no partial timings, so a stacked STT · LLM · TTS bar would be
fabricated rather than measured. The end-to-end figure is also the only one the
person on the call can feel.
It includes the tail of speech-recognition finalization, so it is not time-to-first-token. Do not label it as such.
Microphone
connect() asks for the microphone by default. If you already hold a stream —
for a level meter, say — pass it and it will be reused rather than requesting a
second one:
const call = await ZavuVoice.connect({ token, micStream });Pass requestMicrophone: false to skip the prompt entirely.
Ending a call
hangup() closes the audio and tells the server, so the balance hold is settled
immediately. If the tab is closed instead, a server-side sweep closes the session
and bills the same measured duration — the clock is always kept server-side, so
neither a client call nor a closed laptop can bill more than the cap you set.
License
MIT
