Voice·续

return ( <div> <textarea value={transcript + (interimTranscript ? " " + interimTranscript : "")} readOnly /> <button onClick={isListening ? stop : start}> {isListening ? "Stop" : "Dictate"} </button> </div> ); }


## Client API: `VoiceClient`

Framework-agnostic client for environments without React.

```js
import { VoiceClient } from "@cloudflare/voice/client";

const client = new VoiceClient({ agent: "MyAgent" });

client.addEventListener("statuschange", (status) => {
    console.log("Status:", status);
});

client.addEventListener("transcriptchange", (messages) => {
    console.log("Transcript:", messages);
});

client.addEventListener("error", (err) => {
    console.error("Error:", err);
});

client.connect();
await client.startCall();

// Switch assistant playback without reconnecting the call.
await client.setOutputDevice(selectedSpeakerId);

// Later:
client.endCall();
client.disconnect();
import { VoiceClient } from "@cloudflare/voice/client";

const client = new VoiceClient({ agent: "MyAgent" });

client.addEventListener("statuschange", (status) => {
    console.log("Status:", status);
});

client.addEventListener("transcriptchange", (messages) => {
    console.log("Transcript:", messages);
});

client.addEventListener("error", (err) => {
    console.error("Error:", err);
});

client.connect();
await client.startCall();

// Switch assistant playback without reconnecting the call.
await client.setOutputDevice(selectedSpeakerId);

// Later:
client.endCall();
client.disconnect();

Events

Event Data type Description
statuschange VoiceStatus Pipeline state changed
transcriptchange TranscriptMessage[] Transcript updated
interimtranscript string \| null Interim transcript from streaming STT
metricschange VoicePipelineMetrics Pipeline timing metrics
audiolevelchange number Mic audio level (0–1)
connectionchange boolean WebSocket connected/disconnected
mutechange boolean Mute state changed
error string \| null Error occurred
outputdeviceerror string \| null Non-fatal speaker routing issue
custommessage unknown Non-voice message from server

Advanced options

Option Type Description
transport VoiceTransport Custom transport (default: WebSocket via PartySocket)
audioInput VoiceAudioInput Custom mic capture (default: built-in AudioWorklet)
preferredFormat VoiceAudioFormat Hint for server audio format (advisory only)
outputDeviceId string Preferred audiooutput device for assistant playback

Providers

Built-in (Workers AI)

No API keys required — use your Workers AI binding:

Class Type Default model Recommended for
WorkersAIFluxSTT Continuous STT @cf/deepgram/flux withVoice
WorkersAINova3STT Continuous STT @cf/deepgram/nova-3 withVoiceInput
WorkersAITTS TTS @cf/deepgram/aura-1 Both
import { Agent } from "agents";
import {
    withVoice,
    WorkersAIFluxSTT,
    WorkersAINova3STT,
    WorkersAITTS,
} from "@cloudflare/voice";

const VoiceAgent = withVoice(Agent);

// Default usage
export class MyAgent extends VoiceAgent {
    transcriber = new WorkersAIFluxSTT(this.env.AI);
    tts = new WorkersAITTS(this.env.AI);
}

// Custom options
export class CustomAgent extends VoiceAgent {
    transcriber = new WorkersAIFluxSTT(this.env.AI, {
        eotThreshold: 0.8,
        keyterms: ["Cloudflare", "Workers"],
    });
    tts = new WorkersAITTS(this.env.AI, {
        model: "@cf/deepgram/aura-1",
        speaker: "asteria",
    });
}
import { Agent } from "agents";
import {
    withVoice,
    WorkersAIFluxSTT,
    WorkersAINova3STT,
    WorkersAITTS,
} from "@cloudflare/voice";

const VoiceAgent = withVoice(Agent);

// Default usage
export class MyAgent extends VoiceAgent&lt;Env> {
    transcriber = new WorkersAIFluxSTT(this.env.AI);
    tts = new WorkersAITTS(this.env.AI);
}

// Custom options
export class CustomAgent extends VoiceAgent&lt;Env> {
    transcriber = new WorkersAIFluxSTT(this.env.AI, {
        eotThreshold: 0.8,
        keyterms: ["Cloudflare", "Workers"],
    });
    tts = new WorkersAITTS(this.env.AI, {
        model: "@cf/deepgram/aura-1",
        speaker: "asteria",
    });
}

Third-party providers

Package Class Description
@cloudflare/voice-deepgram DeepgramSTT Continuous STT
@cloudflare/voice-elevenlabs ElevenLabsTTS High-quality TTS
@cloudflare/voice-telnyx TelnyxSTT, TelnyxTTS STT, TTS, and telephony
@cloudflare/voice-twilio TwilioAdapter Telephony (phone calls)
@cloudflare/voice-plivo PlivoAdapter Telephony (phone calls)

ElevenLabs TTS:

import { ElevenLabsTTS } from "@cloudflare/voice-elevenlabs";

export class MyAgent extends VoiceAgent {
    transcriber = new WorkersAIFluxSTT(this.env.AI);
    tts = new ElevenLabsTTS({
        apiKey: this.env.ELEVENLABS_API_KEY,
        voiceId: "21m00Tcm4TlvDq8ikWAM",
    });
}
import { ElevenLabsTTS } from "@cloudflare/voice-elevenlabs";

export class MyAgent extends VoiceAgent&lt;Env> {
    transcriber = new WorkersAIFluxSTT(this.env.AI);
    tts = new ElevenLabsTTS({
        apiKey: this.env.ELEVENLABS_API_KEY,
        voiceId: "21m00Tcm4TlvDq8ikWAM",
    });
}

Deepgram STT:

import { DeepgramSTT } from "@cloudflare/voice-deepgram";

export class MyAgent extends VoiceAgent {
    transcriber = new DeepgramSTT({
        apiKey: this.env.DEEPGRAM_API_KEY,
    });
    tts = new WorkersAITTS(this.env.AI);
}
import { DeepgramSTT } from "@cloudflare/voice-deepgram";

export class MyAgent extends VoiceAgent&lt;Env> {
    transcriber = new DeepgramSTT({
        apiKey: this.env.DEEPGRAM_API_KEY,
    });
    tts = new WorkersAITTS(this.env.AI);
}

Telnyx STT and TTS:

Import from the /stt and /tts subpaths, which are server-safe:

import { TelnyxSTT } from "@cloudflare/voice-telnyx/stt";
import { TelnyxTTS } from "@cloudflare/voice-telnyx/tts";

export class MyAgent extends VoiceAgent {
    transcriber = new TelnyxSTT({
        apiKey: this.env.TELNYX_API_KEY,
        engine: "Telnyx", // or "Deepgram"
        interimResults: true,
    });
    tts = new TelnyxTTS({
        apiKey: this.env.TELNYX_API_KEY,
        voice: "Telnyx.NaturalHD.astra",
    });
}
import { TelnyxSTT } from "@cloudflare/voice-telnyx/stt";
import { TelnyxTTS } from "@cloudflare/voice-telnyx/tts";

export class MyAgent extends VoiceAgent&lt;Env> {
    transcriber = new TelnyxSTT({
        apiKey: this.env.TELNYX_API_KEY,
        engine: "Telnyx", // or "Deepgram"
        interimResults: true,
    });
    tts = new TelnyxTTS({
        apiKey: this.env.TELNYX_API_KEY,
        voice: "Telnyx.NaturalHD.astra",
    });
}

TelnyxTTS defaults to backend: "rest". Set backend: "websocket" for lower time-to-first-audio. That backend requires the Workers runtime.

Telephony

Telephony connects phone calls to the same withVoice agent that serves your browser clients. The call shares that agent instance's conversation history, state, tools, and schedules, so one agent can answer the phone and the web.

Providers take one of two approaches, which determines where call audio arrives and what you have to deploy:

Provider Approach Call audio arrives at Best for
Twilio Server-side adapter in your Worker Your Worker Inbound numbers answered server-side
Plivo Server-side adapter in your Worker Your Worker Inbound numbers answered server-side
Telnyx Browser WebRTC bridge The browser Softphone and click-to-call in an app you ship

Server-side adapters (Twilio and Plivo)

Install the adapter for your provider:

npmyarnpnpmbun

npm i @cloudflare/voice-twilio
yarn add @cloudflare/voice-twilio
pnpm add @cloudflare/voice-twilio
bun add @cloudflare/voice-twilio

npmyarnpnpmbun

npm i @cloudflare/voice-plivo
yarn add @cloudflare/voice-plivo
pnpm add @cloudflare/voice-plivo
bun add @cloudflare/voice-plivo

The adapter terminates the provider's audio WebSocket in your Worker and converts between the provider's 8 kHz mulaw audio and the agent's 16 kHz PCM protocol:

Phone → provider → WebSocket → adapter → WebSocket → VoiceAgent

No browser is involved. Each adapter exposes a handleRequest() method that you call from your fetch handler for the provider's WebSocket path, and by default each call gets its own agent instance named after the provider's call identifier.

Beyond that path, the two providers differ in what they need from you. Twilio is configured with TwiML that points at your Worker. Plivo needs your auth ID, auth token, and phone number — deploying automatically provisions the Plivo application and points its answer URL at your Worker, so neither needs manual setup in the Plivo console.

Browser WebRTC bridge (Telnyx)

npmyarnpnpmbun

npm i @cloudflare/voice-telnyx
yarn add @cloudflare/voice-telnyx
pnpm add @cloudflare/voice-telnyx
bun add @cloudflare/voice-telnyx

Telnyx bridges the PSTN call through WebRTC in the browser and reuses your existing voice client transport:

Phone ↔ Telnyx ↔ WebRTC ↔ browser bridge ↔ WebSocket → VoiceAgent

Because the browser holds the WebRTC session, it needs a short-lived Telnyx credential — never your API key. TelnyxJWTEndpoint mints those tokens server-side and requires an authorize callback, so a public route cannot mint credentials for arbitrary callers. Telephony needs TELNYX_CREDENTIAL_CONNECTION_ID alongside TELNYX_API_KEY.

Telnyx also provides STT and TTS, so it can supply the whole pipeline. Refer to Third-party providers for those.

PCM output for telephony

WorkersAITTS returns MP3, which cannot be decoded to PCM in the Workers runtime. With the Twilio or Plivo adapter, use a TTS provider that outputs raw PCM — for example ElevenLabs with outputFormat: "pcm_16000", or a Workers AI model called with encoding: "linear16" and container: "none".

This constraint does not apply to Telnyx, where the browser decodes audio before playback.

Complete examples

Each adapter ships a runnable example with the Worker routes, provider configuration, and deployment steps:

Plivo voice agent

Answer inbound Plivo calls in a Worker, including the answer URL and application setup.

Telnyx voice agent

Bridge PSTN calls through the browser, including the JWT endpoint and client wiring.

Text messages

withVoice agents can also receive text messages, bypassing STT entirely. This is useful for chat-style input alongside voice.

const { sendText } = useVoiceAgent({ agent: "MyAgent" });

// Send text — goes straight to onTurn() without STT
sendText("What is the weather like today?");

Text messages work both during and outside of active calls. During a call, the response is spoken aloud via TTS. Outside a call, the response is sent as text-only transcript messages.

Custom messages

Send and receive application-level JSON messages alongside voice protocol messages. Non-voice messages pass through to your onMessage handler on the server and emit custommessage events on the client.

Server:

export class MyAgent extends VoiceAgent {
    onMessage(connection, message) {
        const data = JSON.parse(message);
        if (data.type === "kick_speaker") {
            this.forceEndCall(connection);
        }
    }
}
export class MyAgent extends VoiceAgent&lt;Env> {
    onMessage(connection: Connection, message: WSMessage) {
        const data = JSON.parse(message as string);
        if (data.type === "kick_speaker") {
            this.forceEndCall(connection);
        }
    }
}

Client:

const { sendJSON, lastCustomMessage } = useVoiceAgent({ agent: "MyAgent" });

sendJSON({ type: "kick_speaker" });

useEffect(() => {
    if (lastCustomMessage) {
        console.log("Custom message:", lastCustomMessage);
    }
}, [lastCustomMessage]);

Single-speaker enforcement

Use beforeCallStart to restrict who can start a call. This example enforces single-speaker — only one connection can be the active speaker at a time:

import {} from "agents";

export class MyAgent extends VoiceAgent {
    #speakerId = null;

beforeCallStart(connection) {
        if (this.#speakerId !== null) {
            return false;
        }
        this.#speakerId = connection.id;
        return true;
    }

onCallEnd(connection) {
        if (this.#speakerId === connection.id) {
            this.#speakerId = null;
        }
    }
}
import { type Connection } from "agents";

export class MyAgent extends VoiceAgent&lt;Env> {
    #speakerId: string | null = null;

beforeCallStart(connection: Connection) {
        if (this.#speakerId !== null) {
            return false;
        }
        this.#speakerId = connection.id;
        return true;
    }

onCallEnd(connection: Connection) {
        if (this.#speakerId === connection.id) {
            this.#speakerId = null;
        }
    }
}

Pipeline metrics

withVoice agents emit timing metrics after each turn:

const { metrics } = useVoiceAgent({ agent: "MyAgent" });

// metrics: {
//   llm_ms: 850,
//   tts_ms: 200,
//   first_audio_ms: 950,
//   total_ms: 1200,
// }

Conversation history

withVoice automatically persists conversation messages to SQLite. In onTurn(), context.messages is a snapshot of the completed history before the current transcript. The pipeline persists the current transcript before invoking the hook. Therefore, a direct getConversationHistory() call inside onTurn() includes it.

// Get stored history, including the current transcript during onTurn()
const history = this.getConversationHistory(20);

this.saveMessage("assistant", "Welcome! How can I help?");
// Get stored history, including the current transcript during onTurn()
const history = this.getConversationHistory(20);

this.saveMessage("assistant", "Welcome! How can I help?");

History survives Durable Object restarts and client reconnections. Voice agents use keepAlive to prevent eviction during active calls.

Was this helpful?

YesNo

On this page

Voice·续 - 图1Docs

{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/communication-channels/voice/#page","headline":"Voice","description":"Build real-time voice agents with speech-to-text, text-to-speech, and conversation persistence over WebSocket.","url":"https://developers.cloudflare.com/agents/communication-channels/voice/","inLanguage":"en","image":"https://developers.cloudflare.com/agents/communication-channels/voice/og.png?v=def5f75be26dd0bb","dateModified":"2026-10-21","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}


description: Receive and route webhook events from external services to dedicated Cloudflare Agent instances. title: Webhooks image: https://developers.cloudflare.com/agents/communication-channels/webhooks/og.png?v=3407b6ff09d39a33


Skip to content

Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt
Use this file to discover all available pages before exploring further.