The ElevenLabs adapter is voice-focused. It exposes five capabilities:
It does not support text chat() or summarize() — use OpenAI, Anthropic, or Gemini for those.
The realtime adapter uses an agent-based architecture where you configure your conversational AI agent in the ElevenLabs dashboard (voice, personality, knowledge base, tools) and then connect to it at runtime. The adapter wraps the @elevenlabs/client SDK for seamless integration with useRealtimeChat and RealtimeClient.
npm i @tanstack/ai-elevenlabspnpm add @tanstack/ai-elevenlabsyarn add @tanstack/ai-elevenlabsbun add @tanstack/ai-elevenlabsPeer dependencies:
npm i @tanstack/ai @tanstack/ai-clientpnpm add @tanstack/ai @tanstack/ai-clientyarn add @tanstack/ai @tanstack/ai-clientbun add @tanstack/ai @tanstack/ai-clientThe server generates a signed WebSocket URL so your API key never reaches the client. The signed URL is valid for 30 minutes.
import { realtimeToken } from '@tanstack/ai'
import { elevenlabsRealtimeToken } from '@tanstack/ai-elevenlabs'
// In your API route (Express, Hono, TanStack Start, etc.)
export async function POST() {
const token = await realtimeToken({
adapter: elevenlabsRealtimeToken({
agentId: process.env.ELEVENLABS_AGENT_ID!,
}),
})
return Response.json(token)
}You can override agent settings at token generation time without changing your dashboard configuration:
const token = await realtimeToken({
adapter: elevenlabsRealtimeToken({
agentId: process.env.ELEVENLABS_AGENT_ID!,
overrides: {
voiceId: 'custom-voice-id',
systemPrompt: 'You are a helpful voice assistant.',
firstMessage: 'Hello! How can I help you today?',
language: 'en',
},
}),
})import { useRealtimeChat } from '@tanstack/ai-react'
import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'
function VoiceChat() {
const {
status,
mode,
messages,
connect,
disconnect,
pendingUserTranscript,
pendingAssistantTranscript,
inputLevel,
outputLevel,
} = useRealtimeChat({
getToken: () =>
fetch('/api/realtime-token', { method: 'POST' }).then((r) => r.json()),
adapter: elevenlabsRealtime(),
})
return (
<div>
<p>Status: {status}</p>
<p>Mode: {mode}</p>
<button onClick={status === 'idle' ? connect : disconnect}>
{status === 'idle' ? 'Start Conversation' : 'End Conversation'}
</button>
{pendingUserTranscript && <p>You: {pendingUserTranscript}...</p>}
{pendingAssistantTranscript && (
<p>AI: {pendingAssistantTranscript}...</p>
)}
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong>
{msg.parts.map((part, i) => (
<span key={i}>
{part.type === 'text' ? part.content : null}
{part.type === 'audio' ? part.transcript : null}
</span>
))}
</div>
))}
</div>
)
}import { RealtimeClient } from '@tanstack/ai-client'
import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'
const client = new RealtimeClient({
getToken: () =>
fetch('/api/realtime-token', { method: 'POST' }).then((r) => r.json()),
adapter: elevenlabsRealtime(),
onMessage: (message) => {
console.log(`${message.role}:`, message.parts)
},
onStatusChange: (status) => {
console.log('Status:', status)
},
onModeChange: (mode) => {
console.log('Mode:', mode)
},
})
await client.connect()ElevenLabs supports client-side tools that execute in the browser. Define tools using the standard toolDefinition() API:
import { toolDefinition } from '@tanstack/ai'
import { useRealtimeChat } from '@tanstack/ai-react'
import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'
import { z } from 'zod'
const getWeatherDef = toolDefinition({
name: 'getWeather',
description: 'Get weather for a location',
inputSchema: z.object({
location: z.string(),
}),
outputSchema: z.object({
temperature: z.number(),
conditions: z.string(),
}),
})
const getWeather = getWeatherDef.client(async ({ location }) => {
const res = await fetch(`/api/weather?location=${location}`)
return res.json()
})
// Pass tools to the hook
const chat = useRealtimeChat({
getToken: () =>
fetch('/api/realtime-token', { method: 'POST' }).then((r) => r.json()),
adapter: elevenlabsRealtime(),
tools: [getWeather],
})Tool results are automatically serialized to strings and returned to the ElevenLabs agent. The adapter converts TanStack tool definitions into the @elevenlabs/client clientTools format internally.
Used on the server to generate a signed WebSocket URL.
| Option | Type | Required | Description |
|---|---|---|---|
| agentId | string | No* | Agent ID configured in the ElevenLabs dashboard. *Falls back to ELEVENLABS_AGENT_ID; required only if that env var is unset |
| overrides.voiceId | string | No | Custom voice ID to override the agent's default voice |
| overrides.systemPrompt | string | No | Custom system prompt to override the agent's default |
| overrides.firstMessage | string | No | First message the agent speaks when the session starts |
| overrides.language | string | No | Language code (e.g., 'en', 'es', 'fr') |
Used on the client to establish the connection.
| Option | Type | Default | Description |
|---|---|---|---|
| connectionMode | 'websocket' | 'webrtc' | auto-detect | Transport protocol for the connection |
| debug | boolean | DebugConfig | false | Enable debug logging — pass true for all categories, or a DebugConfig to select categories/sink |
ElevenLabs and OpenAI take different approaches to realtime voice:
| ElevenLabs | OpenAI | |
|---|---|---|
| Configuration | Agent-based. Configure voice, personality, and knowledge in the ElevenLabs dashboard or via overrides at token time. | Session-based. Configure instructions, voice, temperature, etc. per session via useRealtimeChat options. |
| Token type | Signed WebSocket URL (valid 30 minutes) | Ephemeral API token (valid ~10 minutes) |
| Transport | WebSocket (default) or WebRTC | WebRTC |
| Audio handling | @elevenlabs/client SDK manages audio capture and playback automatically | TanStack AI manages WebRTC peer connection and audio tracks |
| VAD | Handled by ElevenLabs server-side | Supports server, semantic, and manual modes |
| Runtime updates | Session config is set at creation time and cannot be changed mid-session | Supports updateSession() for mid-session config changes |
| Image input | Not supported | Supported via sendImage() |
| Time domain data | Not available from the SDK | Available for waveform visualizations |
The ElevenLabs adapter provides audio visualization data through the same interface as other realtime adapters:
import { useRealtimeChat } from '@tanstack/ai-react'
import { elevenlabsRealtime } from '@tanstack/ai-elevenlabs'
const {
inputLevel, // 0-1 normalized microphone volume
outputLevel, // 0-1 normalized speaker volume
getInputFrequencyData, // Uint8Array frequency spectrum
getOutputFrequencyData,
} = useRealtimeChat({
getToken: () =>
fetch('/api/realtime-token', { method: 'POST' }).then((r) => r.json()),
adapter: elevenlabsRealtime(),
})Note: ElevenLabs provides volume levels and frequency data but does not expose time-domain data. The getInputTimeDomainData() and getOutputTimeDomainData() methods return static placeholder arrays. The default audio sample rate is 16kHz.
Set these in your server environment:
ELEVENLABS_API_KEY=your-elevenlabs-api-key
ELEVENLABS_AGENT_ID=your-agent-id| Variable | Required | Description |
|---|---|---|
| ELEVENLABS_API_KEY | Yes | Your ElevenLabs API key, used server-side for generating signed URLs |
| ELEVENLABS_AGENT_ID | No | Default agent ID. Can also be passed directly to elevenlabsRealtimeToken() |
Get your API key from the ElevenLabs dashboard. Create and configure agents in the Conversational AI section of the dashboard.
For one-shot speech generation (not realtime), use elevenlabsSpeech with generateSpeech():
The format option supports mp3 (default), pcm, opus, and wav. WAV output contains 44.1 kHz, 16-bit mono PCM with a RIFF header. Requests for aac or flac throw before the API call. An explicit modelOptions.outputFormat overrides format and returns the selected provider format without WAV wrapping.
import { generateSpeech } from "@tanstack/ai";
import { elevenlabsSpeech } from "@tanstack/ai-elevenlabs";
const result = await generateSpeech({
adapter: elevenlabsSpeech("eleven_v3"),
text: "Hello from ElevenLabs!",
voice: "Rachel",
format: "mp3",
});
console.log(result.audio); // Base64-encoded audioElevenLabs has a separate dialogue endpoint that takes up to 10 distinct voices, and a timestamped twin of each endpoint. turns and timestamps pick between them, so you never choose an endpoint by hand:
import { generateSpeech } from "@tanstack/ai";
import { elevenlabsSpeech } from "@tanstack/ai-elevenlabs";
const result = await generateSpeech({
adapter: elevenlabsSpeech("eleven_v3"),
turns: [
{ text: "Knock knock.", voice: "bYTqZQo3Jz7LQtmGTgwi" },
{ text: "Who is there?", voice: "6lCwbsX1yVjD49QmpkTR" },
],
timestamps: true,
});
// Character timings for the whole clip.
console.log(result.alignment?.unit); // 'character'
// One entry per turn, with the voice that spoke it.
for (const segment of result.segments ?? []) {
console.log(segment.turnIndex, segment.voice, segment.text);
}segments only comes back from the dialogue endpoint. A single-voice request with timestamps: true returns alignment alone.
elevenlabsAudio covers both music generation and sound effects depending on the model:
import { generateAudio } from "@tanstack/ai";
import { elevenlabsAudio } from "@tanstack/ai-elevenlabs";
// Music generation
const music = await generateAudio({
adapter: elevenlabsAudio("music_v2_5"),
prompt: "An upbeat synthwave track for a product launch",
});
// Sound effects
const sfx = await generateAudio({
adapter: elevenlabsAudio("eleven_text_to_sound_v2"),
prompt: "A glass shattering on concrete",
});elevenlabsSpeech can read the account's voice catalog, which includes anything generateVoice() has saved:
import { listVoices } from "@tanstack/ai";
import { elevenlabsSpeech } from "@tanstack/ai-elevenlabs";
const { voices } = await listVoices({
adapter: elevenlabsSpeech("eleven_v3"),
origins: ["generated", "cloned"],
});ElevenLabs has no origin filter on GET /v1/voices, so the adapter fetches the catalog and narrows in memory. Its famous and high_quality categories both read as professional.
elevenlabsVoiceDesign creates a new voice from a description. The voice IDs it returns go straight into generateSpeech():
import { generateVoice } from "@tanstack/ai";
import { elevenlabsVoiceDesign } from "@tanstack/ai-elevenlabs";
const result = await generateVoice({
adapter: elevenlabsVoiceDesign("eleven_ttv_v3"),
prompt: "A warm, gravelly narrator in his sixties with a slight Irish lilt",
name: "Irish Narrator",
});
const [voice] = result.voices;
if (!voice) throw new Error("The provider returned no voices.");
console.log(voice.voiceId);
console.log(voice.saved); // true, because a name was givenWithout a name, you get preview voices to audition. With a name, the best candidate is kept in your ElevenLabs voice library.
eleven_ttv_v3 also accepts referenceAudio, a clip of a real speaker used as a design reference — it still needs a prompt, and modelOptions.promptStrength balances the two. eleven_multilingual_ttv_v2 takes no reference audio. See Voice Creation for the full guide.
| Family | Models |
|---|---|
| Text-to-speech | eleven_v3, eleven_v3_conversational, eleven_multilingual_v2, eleven_flash_v2_5, eleven_flash_v2 |
| Music | music_v2_5, music_v2 |
| Sound effects | eleven_text_to_sound_v2 |
| Transcription | scribe_v2, scribe_v2_medical |
| Voice design | eleven_ttv_v3, eleven_multilingual_ttv_v2 |
ElevenLabs deprecated eleven_turbo_v2, eleven_turbo_v2_5, eleven_monolingual_v1, scribe_v1, and music_v1. The adapter still accepts them, and each is commented as deprecated in model-meta.ts. Move to the current model in the same row.
Transcribe audio with elevenlabsTranscription:
import { generateTranscription } from "@tanstack/ai";
import { elevenlabsTranscription } from "@tanstack/ai-elevenlabs";
import { audioFile } from "./audio";
const result = await generateTranscription({
adapter: elevenlabsTranscription("scribe_v2"),
audio: audioFile,
});
console.log(result.text);Route every request through a gateway, such as Cloudflare AI Gateway or a corporate proxy, with baseURL and defaultHeaders. These two option names are the same on every TanStack AI adapter, so one gateway config works for all of them.
import { createElevenLabsSpeech } from "@tanstack/ai-elevenlabs";
const adapter = createElevenLabsSpeech("eleven_v3", process.env.ELEVENLABS_API_KEY!, {
baseURL: "https://gateway.example.com/elevenlabs",
defaultHeaders: { "cf-aig-authorization": `Bearer ${process.env.GATEWAY_TOKEN}` },
});This applies to the speech, audio, and transcription adapters. baseUrl and headers are aliases of the same two options. If you set both forms, baseURL and defaultHeaders win.
Creates an ElevenLabs realtime token adapter for server-side use with realtimeToken().
Parameters:
Returns: A RealtimeTokenAdapter for use with realtimeToken().
Creates an ElevenLabs realtime client adapter for use with useRealtimeChat or RealtimeClient.
Parameters:
Returns: A RealtimeAdapter for use with useRealtimeChat() or RealtimeClient.
Creates an ElevenLabs text-to-speech adapter for use with generateSpeech().
Creates an ElevenLabs audio adapter that covers both music generation and sound effects (selected via the model id) for use with generateAudio().
Creates an ElevenLabs transcription adapter for use with generateTranscription().
Creates an ElevenLabs voice-design adapter for use with generateVoice().
Reads the account's voice catalog via GET /v1/voices, for use with listVoices().