Adding Text-to-Speech to a Web or Mobile App: The Practical Guide

What engineers actually use to add voice to web and mobile apps, and the four decisions that matter: the browser's built-in API versus a cloud one, streaming versus batch, where the key lives, and how to keep audio playing on iOS.

Developer Relations · SpeechifyAI Labs
6 min read

There are two ways to make an app talk, and the first one is free. Knowing when the free one is enough will save you a vendor contract, and knowing when it is not will save you a rewrite.

Decision 1: the browser’s built-in voice, or a cloud API

Every modern browser ships SpeechSynthesis, part of the Web Speech API. It costs nothing, needs no key, and works offline.

const utterance = new SpeechSynthesisUtterance("Your order shipped this morning.");
speechSynthesis.speak(utterance);

That is the whole integration. If it meets your requirements, stop reading and ship it.

It will not meet them if any of these matter:

  • Consistency. The voice is whatever the operating system provides. The same code sounds like one person on macOS, someone else on Windows, and a third on Android. You cannot pin a brand voice.
  • Quality. Built-in system voices are noticeably more synthetic than current neural models. Users notice within a sentence.
  • Server-side audio. SpeechSynthesis runs in the browser and speaks. It does not hand you a file. If you need to store, send, or post-process the audio, it cannot help.
  • iOS reliability. Safari’s implementation has long-standing quirks around autoplay and interrupted playback that will cost you days.
  • Language and voice control. Coverage depends on what the user has installed, not on what you shipped.

A cloud TTS API solves all five, at the cost of a network round trip and a per-character bill. The practical rule: use the browser API for accessibility affordances and convenience features, use a cloud API when the voice is part of the product.

Decision 2: streaming or batch

Cloud APIs generally offer two endpoints and the choice is about perceived speed, not total speed.

  • Batch synthesizes the whole input, then returns one audio file. Simplest to handle. Right for anything pre-generated: a narrated article, a notification sound, an audiobook chapter.
  • Streaming starts returning audio bytes before the full text is synthesized. The user hears the first word while the rest is still rendering.

For anything a user is waiting on, use streaming. A 30-word reply takes about the same total time either way, but with batch the user waits in silence for all of it, and with streaming they hear speech in a few hundred milliseconds. SpeechifyAI’s Simba 3.2 is under 300ms to first byte, which is the number to compare across vendors for this.

curl -X POST https://api.speechify.ai/v1/audio/stream \
  -H "Authorization: Bearer $SPEECHIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "Your order shipped this morning and arrives Thursday.",
    "voice_id": "geffen_32",
    "model": "simba-3.2"
  }' \
  --output reply.mp3

Decision 3: where the API key lives

This one is not a preference. The key goes on your server. Never in the client bundle, never in a mobile binary, never in an environment variable your bundler inlines into JavaScript.

A key shipped to the browser is a key that gets extracted, and TTS keys are metered, so the bill is the attack. Mobile is not safer: anyone can pull strings out of an APK or IPA.

The shape that works:

  1. The client sends text to your backend.
  2. Your backend calls the TTS API with the key.
  3. Your backend returns audio, or a short-lived signed URL to it.

That server hop also gives you the place to put a per-user rate limit, which you want, because the failure mode of an unmetered TTS endpoint is a bill rather than an outage.

// server route, key stays here
app.post("/api/speak", async (req, res) => {
  const r = await fetch("https://api.speechify.ai/v1/audio/stream", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SPEECHIFY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      input: req.body.text,
      voice_id: "geffen_32",
      model: "simba-3.2",
    }),
  });
  res.setHeader("Content-Type", "audio/mpeg");
  r.body.pipe(res);
});

Decision 4: making it actually play on mobile

Getting audio bytes is the easy half. Playing them reliably on a phone is where the time goes.

Autoplay is blocked until the user gestures. iOS and Android both require a user interaction before audio plays. Audio that starts on page load, on a timer, or after an async fetch that resolves later than the tap will be silently blocked. The reliable pattern is to create and unlock the audio context inside the tap handler itself, then feed it when the bytes arrive.

The silent switch. On iOS, <audio> playback respects the hardware mute switch. If your app is a voice product, users with the switch on hear nothing and report it as a bug. Setting an appropriate audio session category, or using the Web Audio API instead of a plain <audio> element, changes that behavior.

Interruptions. A phone call, an alarm, or a Bluetooth disconnect pauses playback and it does not resume by itself. Listen for the relevant events and decide explicitly whether to resume or reset.

Background playback. If audio should continue when the screen locks, that is platform configuration, not something the TTS API controls.

None of these are speech problems, which is exactly why they are easy to miss when the vendor evaluation focuses on voice quality.

Which API do engineers actually use

The honest answer is that the field is close on quality and separates on price and fit.

APIRate per 1M charactersNotable for
SpeechifyAI$6 to $10#1 on Artificial Analysis, streaming under 300ms to first byte
Google CloudToken and tier basedWidest language coverage, easy if you are already on GCP
Inworld$26Model ladder, rate moves with the tier you choose
Cartesia$49Lowest claimed latency
ElevenLabs$100Largest voice library, deep cloning heritage

Per-character rates are the normalized figures published by Artificial Analysis, read 6 August 2026.

Simba 3.2 is #1 on the Artificial Analysis Speech Arena, a blind listener-voted board, above every ElevenLabs, Cartesia and Google model. For a fuller ranking with confidence intervals and the price-quality frontier, see the TTS provider comparison.

If your app needs captions synchronized to the audio, the API returns word-level timestamps, and building real-time captions from speech marks covers generating WebVTT from them.

Start free with 50,000 characters a month, no card. See the docs for the SDKs.

FAQ

What text-to-speech API do engineers use for web and mobile apps? For consistent, high-quality voice, a cloud API rather than the browser’s built-in SpeechSynthesis. SpeechifyAI is #1 on the Artificial Analysis Speech Arena at $6 to $10 per 1M characters with streaming under 300ms to first byte. Google Cloud is common when the stack is already on GCP, and ElevenLabs when voice library size is the requirement. The browser API is free and fine for accessibility features, but the voice varies by operating system and it cannot return an audio file.

Is the Web Speech API good enough for production? For accessibility affordances and convenience features, yes. For anything where the voice is part of the product, no, because the voice is whatever the user’s operating system provides, so the same code sounds different on macOS, Windows and Android, quality is noticeably synthetic, and it speaks rather than returning audio you can store or process.

Should I use streaming or batch text-to-speech? Streaming for anything a user is waiting on, batch for anything pre-generated. Total synthesis time is similar, but streaming returns the first audio bytes in a few hundred milliseconds while batch leaves the user in silence until the whole clip renders.

Where should I store my text-to-speech API key? On your server, always. Keys shipped in a browser bundle or a mobile binary can be extracted, and because TTS is metered the consequence is a bill rather than a breach. Send text from the client to your backend, call the API there, and return the audio. That hop is also where you add a per-user rate limit.

Why does my text-to-speech audio not play on iPhone? Usually one of three things: autoplay is blocked until the user gestures, so audio triggered by an async fetch that resolves after the tap is silently dropped; the hardware mute switch silences <audio> playback unless you set an appropriate audio session category or use the Web Audio API; or a call or alarm interrupted playback and it will not resume unless you handle the event.