Skip to content
Vibe Code Basics
AI Voice

Add AI Voice to Your App with the ElevenLabs API (A Vibe Coder's Guide)

Add realistic text to speech to a Next.js app with the ElevenLabs API: a secure route handler, a Listen button, choosing models, caching audio to save credits, and prompts to have your AI agent build it.

By Vibe Code Basics Editorial TeamPublished 5 min read
On this page (11)

Disclosure: this post contains affiliate links. If you sign up for a paid ElevenLabs plan through them we may earn a commission, at no extra cost to you.

Adding a voice to your app takes one server endpoint and one button. Your server sends text to the ElevenLabs text-to-speech API with a secret key, gets back an MP3, and streams it to the browser. The whole feature is about 40 lines of code, and your AI coding agent can write it in minutes, if you tell it the few things that matter: keep the key on the server, limit who can call it, and cache the audio.

This guide shows the code for a Next.js app, explains each decision, and gives you prompts for Claude Code, Cursor or any other agent. You'll need an ElevenLabs account: sign up free here (opens in a new tab). For the bigger picture on plans and models, see our ElevenLabs review.

What you can build with voice

A few ideas that work well in small apps:

  • A "Listen to this article" button on blog posts or docs.
  • Spoken feedback in a language-learning, fitness or kids' app.
  • Accessibility: reading content aloud for users who prefer audio.
  • Narrated onboarding or product tours.
  • Audio versions of user content, like turning notes or summaries into a short daily briefing.

For two-way conversations (the user talks, the app answers), use ElevenAgents instead; see our voice agent guide.

How it works

  1. The browser sends text to your API route, for example POST /api/speak.
  2. Your route checks the request, then calls POST https://api.elevenlabs.io/v1/text-to-speech/VOICE_ID with your API key in the xi-api-key header.
  3. ElevenLabs returns MP3 audio, which your route passes back to the browser.
  4. The browser plays it.

The browser never sees your API key. That's the single most important rule: never call ElevenLabs directly from client-side code, or anyone can copy your key from the network tab and spend your credits.

Step 1: Get an API key and a voice ID

  1. In your ElevenLabs account, create an API key. Give it only the permissions you need (text to speech) and set a credit limit if the option is offered.
  2. Pick a voice from the voice library and copy its voice ID.
  3. Add both to .env.local (and to your hosting provider's environment variables, such as Vercel's):
ELEVENLABS_API_KEY=your-key-here
ELEVENLABS_VOICE_ID=your-voice-id

Make sure .env.local is in .gitignore. If a key ever gets committed, revoke it and create a new one. Our Git guide covers keeping secrets out of your repo.

Step 2: Create the API route

A Next.js route handler that validates input, calls ElevenLabs, and streams the audio back:

// app/api/speak/route.ts
const MAX_CHARS = 1000;

export async function POST(request: Request) {
  const { text } = await request.json().catch(() => ({}));

  if (typeof text !== 'string' || !text.trim() || text.length > MAX_CHARS) {
    return Response.json({ error: `Send 1 to ${MAX_CHARS} characters of text.` }, { status: 400 });
  }

  const voiceId = process.env.ELEVENLABS_VOICE_ID;
  const res = await fetch(
    `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}?output_format=mp3_44100_128`,
    {
      method: 'POST',
      headers: {
        'xi-api-key': process.env.ELEVENLABS_API_KEY!,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ text, model_id: 'eleven_flash_v2_5' }),
    }
  );

  if (!res.ok) {
    console.error('ElevenLabs error', res.status, await res.text());
    return Response.json({ error: 'Could not generate audio.' }, { status: 502 });
  }

  return new Response(res.body, { headers: { 'Content-Type': 'audio/mpeg' } });
}

Notes on the choices:

  • Character limit. Credits are spent per character, so cap input length. Pick a limit that fits your feature.
  • Model. eleven_flash_v2_5 is fast and cheaper per character, ideal for short UI feedback. For expressive narration use eleven_v4, or eleven_multilingual_v2 for long, consistent multilingual speech. Check the ElevenLabs models docs (opens in a new tab) for current IDs and limits.
  • Error handling. Log the real error on the server, return a generic message to the user.
  • Streaming. Passing res.body straight through lets playback start sooner and avoids holding the whole file in memory.

If you prefer an SDK, ElevenLabs publishes an official JavaScript package, @elevenlabs/elevenlabs-js, with a textToSpeech.convert method that does the same thing.

Step 3: Add a Listen button

A small client component that calls your route and plays the result:

// components/ListenButton.tsx
'use client';
import { useState } from 'react';

export default function ListenButton({ text }: { text: string }) {
  const [loading, setLoading] = useState(false);

  async function play() {
    setLoading(true);
    try {
      const res = await fetch('/api/speak', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text }),
      });
      if (!res.ok) throw new Error('Audio request failed');
      const url = URL.createObjectURL(await res.blob());
      const audio = new Audio(url);
      audio.onended = () => URL.revokeObjectURL(url);
      await audio.play();
    } catch (err) {
      console.error(err);
      alert('Sorry, audio is unavailable right now.');
    } finally {
      setLoading(false);
    }
  }

  return (
    <button onClick={play} disabled={loading}>
      {loading ? 'Loading audio…' : '▶ Listen'}
    </button>
  );
}

Drop <ListenButton text={post.summary} /> anywhere you want spoken content.

Step 4: Protect your credits

A public endpoint that spends money is a target. Before you ship:

  • Require sign-in for the route if the feature is for logged-in users (see Next.js authentication).
  • Rate limit per user or IP, for example a few requests per minute, using your platform's rate limiting or a small Redis-based limiter.
  • Set a spending cap on the API key in your ElevenLabs settings, and monitor usage.
  • Only speak text you control where possible. If users can send arbitrary text, you're paying for whatever they send.

Run through our vibe coding security checklist before launch; an unprotected paid API route is one of the most common mistakes in AI-built apps.

Step 5: Cache audio you've already generated

If the same text gets spoken more than once (a blog post, a fixed onboarding script), generate it once and store the MP3:

  1. Hash the text plus voice ID and model to make a cache key.
  2. Check your storage (Vercel Blob, S3, Supabase Storage) for that key.
  3. If it exists, return the stored file's URL. If not, call ElevenLabs, save the result, and return it.

For a blog, you can go further and generate the audio at publish time, so readers never wait and you pay once per article. This one change can cut your ElevenLabs bill dramatically.

Prompts for your coding agent

Paste this into Claude Code, Cursor or your agent of choice:

Add text-to-speech using the ElevenLabs API.
- Create a POST route at app/api/speak/route.ts that accepts { text }, rejects empty
  text or more than 1000 characters, and calls
  https://api.elevenlabs.io/v1/text-to-speech/{ELEVENLABS_VOICE_ID} with the
  xi-api-key header from ELEVENLABS_API_KEY. Use model_id eleven_flash_v2_5 and
  stream the MP3 response back with Content-Type audio/mpeg.
- Never expose the API key to the client. Read both values from environment variables.
- Require an authenticated user and add a rate limit of 5 requests per minute per user.
- Create a ListenButton client component that calls the route and plays the audio,
  with loading and error states.
- Cache generated audio in [Vercel Blob / Supabase Storage] keyed by a SHA-256 hash of
  text + voice ID + model, and return the cached file when it exists.
Show me the plan before writing code.

Then review what it builds: check the key is only read on the server, test the length limit and rate limit, and listen to the output. Our prompting guide explains why asking for a plan first saves rework.

ElevenLabs also offers an MCP server, so agents that support the Model Context Protocol can generate speech or look up voices while they work.

How much will it cost?

API usage draws from the same monthly credits as the web app. Roughly:

PlanCredits/monthRough audio
Free10,000About 10 minutes (testing only, no commercial use)
Starter, $630,000About 30 minutes
Creator, $22121,000About 2 hours
Pro, $99600,000About 10 hours, PCM output via API

Faster models like Flash use fewer credits per character, so short UI audio goes further. Prototype on the free plan (opens in a new tab), then move to a paid plan before real users (and the commercial license) are involved.

Frequently asked questions

Can I call the ElevenLabs API from the browser?

Not with your secret API key. Anyone could copy it. Always call ElevenLabs from a server route and pass the audio to the browser.

Which ElevenLabs model should my app use?

Use eleven_flash_v2_5 for short, fast UI audio, eleven_v4 for expressive narration, and eleven_multilingual_v2 for long multilingual content. Check the models docs for the latest options.

Is the ElevenLabs API free?

The free plan includes API access with 10,000 credits a month, but no commercial license. Paid plans start at $6 a month.

How do I stop people from running up my ElevenLabs bill?

Require sign-in, rate limit the route, cap input length, set a spending limit on the API key, and cache audio you've already generated.

Ship it

Voice is one of the quickest ways to make a small app feel polished. Grab an ElevenLabs API key (opens in a new tab), paste the prompt above into your agent, and you can have a working Listen button before lunch.

  • #ElevenLabs
  • #AI Voice
  • #Text to Speech
  • #Next.js
  • #API
  • #Vibe Coding

Keep reading