Skip to content

useAgentCall gives a React component a call to your agent: its live state to render, and the controls to drive it. It works with React 18 and 19, Next.js (App and Pages Router), Remix and Vite.

npm install @voice213/agent-client
"use client";
import { useAgentCall } from "@voice213/agent-client/react";

export function TalkToUs() {
  const { status, isSpeaking, isMuted, transcript, partial, error, elapsedSeconds, start, end, toggleMute } = useAgentCall({
    agentId: "k3x9m2p7q4wz",
  });

  if (status !== "connected") {
    return (
      <div>
        <button onClick={start} disabled={status === "connecting"}>
          {status === "connecting" ? "Connecting…" : status === "ended" ? "Call again" : "Talk to us"}
        </button>
        {error && <p role="alert">{error.message}</p>}
      </div>
    );
  }

  return (
    <div>
      <p>
        {isSpeaking ? "Speaking…" : "Listening…"} · {Math.floor(elapsedSeconds / 60)}:{String(elapsedSeconds % 60).padStart(2, "0")}
      </p>
      <button onClick={toggleMute}>{isMuted ? "Unmute" : "Mute"}</button>
      <button onClick={end}>Hang up</button>
      <ul>
        {transcript.map((m) => (
          <li key={m.id} dir="auto">{m.role === "tool" ? `⚙ ${m.text}` : m.text}</li>
        ))}
        {partial.agent && <li dir="auto">{partial.agent}…</li>}
      </ul>
    </div>
  );
}

​What it returns

Everything in the call's state, plus the controls:

FieldType
status"idle" | "connecting" | "connected" | "ended" | "error"Where the call is
isSpeaking, modeboolean, "listening" | "speaking"Whether the agent is talking
isMutedbooleanWhether the caller is muted
transcriptTranscriptMessage[]Completed turns and tool runs: { id, role, text, ok?, at }
partial{ user, agent }The words being spoken right now
toolstring | nullThe tool the agent is running
errorVoice213Error | nullWhy the call failed. See Errors.
endedCallEnded | nullThe summary once it ended: { reason, durationSeconds, billedSeconds, conversationId }
elapsedSecondsnumberSeconds since it connected, updated every second
conversationId, maxSeconds, agent, startedAtAs on AgentCall
start()() => Promise<void>Starts the call. Call it from a click. It never throws: a failure lands in error.
end()() => Promise<CallEnded | null>Hangs up
setMuted(muted), toggleMute()Mute the caller
sendText(text)(text) => booleanSends a typed message
callAgentCallThe underlying client: levels and spectra for visualisers, device switching, events

The options are those of AgentCall, callbacks included (onMessage, onEnd…). They can change from one render to the next, and the call always uses the latest. The call ends when the component unmounts.

​Next.js

The hook uses the microphone and Web Audio, so the component that calls it must be a Client Component: put "use client" at the top of its file. The page around it can stay a Server Component:

import { TalkToUs } from "./TalkToUs"; // the "use client" component above

export default function ContactPage() {
  return (
    <main>
      <h1>Talk to our team</h1>
      <TalkToUs />
    </main>
  );
}

On the server, the hook renders the idle state, so server rendering and hydration need no special care.

​React to the conversation

Use callbacks to act on what happens, such as saving the transcript when the call ends or showing a toast when a tool runs:

const { start } = useAgentCall({
  agentId: "k3x9m2p7q4wz",
  onToolCall: ({ name, status }) => status === "running" && toast(`Checking ${name.replaceAll("_", " ")}…`),
  onEnd: ({ reason, durationSeconds, conversationId }) => analytics.track("agent_call", { reason, durationSeconds, conversationId }),
  onError: (error) => error.code === "MIC_DENIED" && setShowMicHelp(true),
});

For the conversation's summary and outcome, use webhooks: they're written on the server after the call.

​A visualiser

Levels change many times a second. Read them in an animation frame instead of React state, so the page doesn't re-render at audio rate:

"use client";
import { useEffect, useRef } from "react";
import { useAgentCall } from "@voice213/agent-client/react";

export function Orb() {
  const { call, status, start, end } = useAgentCall({ agentId: "k3x9m2p7q4wz" });
  const orb = useRef(null);

  useEffect(() => {
    if (status !== "connected") return;
    let frame;
    const draw = () => {
      const level = call.isSpeaking ? call.getOutputVolume() : call.getInputVolume();
      orb.current.style.transform = `scale(${1 + level * 0.6})`;
      frame = requestAnimationFrame(draw);
    };
    frame = requestAnimationFrame(draw);
    return () => cancelAnimationFrame(frame);
  }, [call, status]);

  return (
    <button onClick={status === "connected" ? end : start} aria-label={status === "connected" ? "Hang up" : "Talk to us"}>
      <span ref={orb} style={{ display: "block", width: 96, height: 96, borderRadius: "50%", background: "linear-gradient(140deg, #FF9A62, #F0436E)", transition: "transform 80ms" }} />
    </button>
  );
}

For a spectrum, use call.getOutputFrequencyData() (128 values, 0–255) the same way.

​Typed messages

sendText lets a visitor type when they can't talk:

function TypeInstead() {
  const { status, sendText } = useAgentCall({ agentId: "k3x9m2p7q4wz" });
  return (
    <form onSubmit={(e) => { e.preventDefault(); if (sendText(e.currentTarget.message.value)) e.currentTarget.reset(); }}>
      <input name="message" placeholder="Type a message" disabled={status !== "connected"} />
    </form>
  );
}

The agent answers it by voice, like speech, and it appears in transcript as the caller's turn.

Was this page helpful?