Simba 3.2 is our recommended TTS model: what changed and how to migrate

8 min read

simba-3.2 is now the recommended Speechify TTS model, and GET /v1/audio/models lets you discover it at runtime instead of hardcoding a model list. What shipped, the voice allow-list, and how to move off simba-3.0.

Developer Relations · SpeechifyAI Labs

simba-3.2 is the model we now recommend for new English TTS integrations. It’s streaming-native, with the lowest time to first byte and the most expressive output of the current lineup. Two changelog entries landed a day apart to support it: simba-3.2 itself on July 8, and GET /v1/audio/models on July 9, a discovery endpoint so you never hardcode a model list again. This post covers what shipped, the one real constraint (a curated voice list), and how to move an existing simba-3.0 integration over.

If you want to follow along with real requests, grab an API key . Every response below is from a live call.

What shipped

simba-3.2 works on the two synthesis endpoints you already use, POST /v1/audio/speech and POST /v1/audio/stream, by passing "model": "simba-3.2". It scored top of the pile on independent benchmarks, which we covered in the Artificial Analysis leaderboard post, so I won’t rerun those numbers here.

The second entry is the more structural one. GET /v1/audio/models returns the model catalog, so your code can ask the API which models exist and which one to prefer, rather than shipping a hardcoded string that goes stale. Here is the real response, trimmed to the fields that matter:

{
  "models": [
    {
      "id": "simba-english",
      "name": "Simba English",
      "default": true,
      "recommended": false,
      "deprecated": true,
      "description": "Legacy Simba 1.6 English model, still the default when a request omits `model`. Prefer simba-3.2.",
      "languages": ["en"]
    },
    {
      "id": "simba-multilingual",
      "name": "Simba Multilingual",
      "default": false,
      "recommended": false,
      "deprecated": true,
      "description": "Legacy Simba 1.6 multilingual model covering 30+ languages. Prefer simba-3.0 for the languages it supports.",
      "languages": ["en", "fr-FR", "de-DE", "es-MX", "..."]
    },
    {
      "id": "simba-3.0",
      "name": "Simba 3.0",
      "default": false,
      "recommended": false,
      "deprecated": false,
      "description": "Streaming-native synthesis in English and six European languages, routed by the request `language`.",
      "languages": ["en", "de-DE", "es-ES", "es-MX", "fr-FR", "it-IT", "pt-BR"]
    },
    {
      "id": "simba-3.2",
      "name": "Simba 3.2",
      "default": false,
      "recommended": true,
      "deprecated": false,
      "description": "Streaming-native model with the lowest time-to-first-byte and richest expressivity, English only today.",
      "languages": ["en"]
    }
  ]
}

Two flags do the work. recommended: true marks the model to reach for, currently simba-3.2. default: true is the model the API uses when a request omits model, and that’s still simba-english, the legacy Simba 1.6 model, now marked deprecated: true. Read that pairing carefully: if you don’t set model, you get the old default, not the recommended one. Setting model explicitly is the whole migration in one sentence, and the rest of this post is the detail around it.

What “streaming-native” means for latency

TTFB is time to first byte: how long after you send the request until the first chunk of audio comes back. For a file you generate once and play later, TTFB barely matters, total time does. For anything a person waits on, a voice reply, a read-aloud that starts the moment they click, TTFB is the number that decides whether it feels instant or laggy.

Time to first byte

Where the first audio byte lands

Both endpoints synthesize the same clip. The difference is when you get the first byte. POST /v1/audio/speech returns the whole file at once, so the first byte is also the last. POST /v1/audio/stream sends audio in chunks, so playback can start while the rest is still being generated.

  1. Full synthesis POST /v1/audio/speech
  2. Streaming POST /v1/audio/stream

For a file you download and play later, the total time is what matters and the two are close. For anything a person waits on, TTFB is the number, and streaming is what keeps it low. simba-3.2 is streaming-native, so its TTFB is the lowest of the lineup.

POST /v1/audio/speech synthesizes the whole clip and returns it in one JSON response, base64-encoded. POST /v1/audio/stream returns raw audio bytes over chunked transfer encoding, so the first bytes arrive while the rest is still being generated. A June 23 clarification pinned down exactly what the stream endpoint returns per Accept header:

AcceptResponse Content-TypeCodec
audio/mpegaudio/mpegMP3, 64 kbps
audio/oggaudio/oggOpus
audio/aacaudio/aacAAC-LC
audio/pcmaudio/L16; rate=24000; channels=1Raw 16-bit signed little-endian PCM

All of them are 24 kHz mono. The audio/pcm row is the one that trips people up: you ask for audio/pcm and the response labels itself audio/L16, which is the IANA-registered name for that raw format. I confirmed it by sending Accept: audio/pcm to the stream endpoint with model: simba-3.2 and reading the response header back as audio/L16; rate=24000; channels=1. For the browser path, the Web Audio streaming post plays these chunks as they land.

The voice allow-list, and what happens if you ignore it

Here’s the one constraint worth being straight about. simba-3.2 doesn’t serve the full 951-voice catalog yet. It serves a curated set of eight registered voices: beatrice_32, dominic_32, edmund_32, geffen_32, harper_32, hugh_32, imogen_32, and wyatt_32. New voices land on the list regularly, which is exactly why the models endpoint (and GET /v1/voices) matters: you read the current list instead of trusting one you wrote down.

Point an off-list voice at simba-3.2 and you get a 400, not audio. I sent voice_id: george (a catalog voice that doesn’t list simba-3.2) and the API returned:

{
  "error": {
    "code": "bad_request",
    "message": "the selected voice is not available for simba-3.2. Choose a voice that lists simba-3.2 in GET /v1/voices, or use simba-english / simba-multilingual."
  },
  "request_id": "ceb6083e736ecbca6910447e"
}

The message tells you the fix. A voice’s models array in GET /v1/voices lists which models it supports, so filter your voice picker by that array and a user can’t select a pairing the API will reject.

Stop hardcoding models: a runtime model picker

The point of GET /v1/audio/models is to pick a model by its properties, not its name. Fetch the list, keep the models that speak your language, and prefer recommended, falling back to default. That’s one small function, and it keeps working when the recommended model changes under you.

In TypeScript:

async function pickModel(language = "en"): Promise<string> {
  const res = await fetch("https://api.speechify.ai/v1/audio/models", {
    headers: { Authorization: `Bearer ${process.env.SPEECHIFY_API_KEY}` },
  });
  if (!res.ok) throw new Error(`GET /v1/audio/models failed: ${res.status}`);
  const { models } = await res.json();

  const forLanguage = models.filter((m) =>
    m.languages.some((l: string) => l === language || l.startsWith(`${language}-`)),
  );

  const recommended = forLanguage.find((m) => m.recommended);
  const fallback = forLanguage.find((m) => m.default) ?? models.find((m) => m.default);
  return (recommended ?? fallback).id;
}

The same shape in Python:

import os
import requests

def pick_model(language: str = "en") -> str:
    res = requests.get(
        "https://api.speechify.ai/v1/audio/models",
        headers={"Authorization": f"Bearer {os.environ['SPEECHIFY_API_KEY']}"},
    )
    res.raise_for_status()
    models = res.json()["models"]

    for_language = [
        m for m in models
        if any(l == language or l.startswith(f"{language}-") for l in m["languages"])
    ]

    recommended = next((m for m in for_language if m["recommended"]), None)
    fallback = next((m for m in for_language if m["default"]), None) or next(
        (m for m in models if m["default"]), None
    )
    return (recommended or fallback)["id"]

Run either against the current catalog with language="en" and you get simba-3.2 back, because it’s the recommended English model today. Ask for a language simba-3.2 doesn’t cover and you fall through to whatever’s recommended or default for it, which is the behaviour you want: no code change when the catalog grows.

Migrating a simba-3.0 integration

If you’re already calling simba-3.0, the move to simba-3.2 for English is small:

  • Change the model field on your speech or stream call from simba-3.0 to simba-3.2. If you’d rather not hardcode it again, drop in the picker above.
  • Check your voice_id is on the simba-3.2 list. If it isn’t, pick one that is, or filter your selector by each voice’s models array. This is the step that actually breaks people, not the model string.
  • If you’re a raw HTTP caller (not on an SDK), pin Speechify-Version so a later wire change doesn’t move you unexpectedly. The version pinning post covers the header; the SDKs send it for you.

Keep simba-3.0 where you need a language simba-3.2 doesn’t cover yet. simba-3.0 still serves English and six European languages routed by the request language, and it isn’t deprecated. simba-3.2 is the English recommendation; simba-3.0 is the multilingual one until simba-3.2 grows past English.

The Python and Node paths are the same call you already make, just with the model swapped, so the Python streaming post and the Node.js TTS post both apply unchanged.

FAQ

simba-3.2. It’s flagged recommended: true in GET /v1/audio/models, it’s streaming-native with the lowest time to first byte, and it’s the most expressive model in the lineup. It’s English-only today. For non-English, use simba-3.0, which covers English plus German, Spanish, French, Italian, and Portuguese and is not deprecated.

Do I have to change my code to use simba-3.2?

Set the model field to simba-3.2 on your POST /v1/audio/speech or POST /v1/audio/stream call, and make sure your voice_id is one the model supports. If you omit model entirely, the API falls back to the default, which is still the legacy simba-english (Simba 1.6), not simba-3.2. So yes: pick the model explicitly, either as a literal or via the models endpoint.

Why did my simba-3.2 request return a 400?

Most likely the voice. simba-3.2 serves a curated allow-list of registered voices (beatrice_32 through wyatt_32 today), not the whole catalog. Sending a voice that doesn’t list simba-3.2 returns 400 bad_request with a message naming the fix. Check the voice’s models array in GET /v1/voices, and filter your voice picker by it so the pairing is always valid.

What does GET /v1/audio/models return?

A models array. Each entry has an id (the string you pass as model), a name, boolean default and recommended flags, a deprecated flag, a description, and a languages list. Read recommended to pick the model to reach for and default to know what a request without a model field resolves to. Fetching it at runtime means you don’t hardcode a model list that goes stale when a new model lands.

Is simba-3.0 deprecated now that simba-3.2 exists?

No. simba-3.0 is not deprecated and stays the model for its languages. What’s deprecated is the older Simba 1.6 lineup, simba-english and simba-multilingual, even though simba-english is still the API-wide default when no model is sent. simba-3.2 is the recommended English model; simba-3.0 remains recommended in practice for the European languages simba-3.2 doesn’t cover yet.

The model reference and the full parameter surface are in the Speechify docs .