Skip to content

Before you launch, make sure your API key stays private, your code handles errors and limits properly, and your API credit can't run dry unnoticed.

​Keep your key on the server

Call Voice213 from your backend, never from browser or mobile app code. A key shipped to a client can be extracted and used to spend your API credit. The API also refuses cross-origin requests from other websites' browser code, so a front-end call fails with a CORS error anyway.

The usual pattern is a small endpoint on your server that your front end calls:

app.post("/api/speak", async (req, res) => {
  // Authenticate your own user here, and validate what they send
  const response = await fetch("https://api.voice213.com/api/v1/tts", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.VOICE213_API_KEY}`,
      "Content-Type": "application/json",
    },
    // voiceId: the inline format, so the WAV comes back in the response
    body: JSON.stringify({ text: String(req.body.text ?? "").slice(0, 3000), model: "voice213-lite", voiceId: "nariman", dialect: 1 }),
  });
  const result = await response.json();
  if (!response.ok) return res.status(response.status).json({ error: result.message ?? result.error });

  // "data:audio/wav;base64,<audio>" → the WAV file
  const [meta, audio] = result.data.audio.split(",");
  res.type(meta.slice(5, meta.indexOf(";"))).send(Buffer.from(audio, "base64"));
});

Put your own authentication and rate limiting in front of an endpoint like this. Otherwise anyone who finds it can spend your API credit through it.

​Protect your key

Each account has one API key. The API Keys page shows when it last made a successful voice-over, and Usage breaks down your calls day by day.

If the key may have leaked, switch it off on the API Keys page right away: requests that use it are refused at once. Then delete it, create a new one, and deploy the new key.

​Handle errors and retry safely

Every error response is JSON with a readable message in error:

{ "error": "Rate limit exceeded: RPM limit exceeded", "limit": 600 }

When your API credit runs out, error holds the code INSUFFICIENT_CREDIT and message the explanation, with the amounts:

{
  "error": "INSUFFICIENT_CREDIT",
  "message": "Not enough API credit. Top up your balance in the developer workspace.",
  "required": 3.6,
  "balance": 0.12,
  "currency": "DZD"
}

Retry only the errors that can succeed on a second attempt:

StatusRetry?What to do
400NoFix the request. The message says what's wrong.
401NoCheck the API key, and that it's sent as Authorization: Bearer <key> or in x-api-key.
402After a top-up, or once running voice-overs endTop up your API credit, then retry. If your other running voice-overs have set the rest of it aside, you can also retry once they end.
403NoThe account is deactivated or suspended, or its API access is switched off (API_ACCESS_BLOCKED). Contact support.
410NoThe endpoint was removed from the API: remove the call. See Removed endpoints.
413NoSend a smaller body (the limit is 25 MB).
429YesWait for the minute (or the day) to pass, or for one of your 20 running voice-overs to end. See Rate Limits.
500, 502YesRetry with exponential backoff.
503LaterVoice-overs through the API are paused, or the API is down for maintenance. For MODEL_DISABLED, switch to another model instead. See Paused models and maintenance.

Voice213 charges a request only once its generation has succeeded, so a failed request costs nothing and retrying it is safe. Here's a small wrapper that retries with backoff:

import os
import time

import requests

RETRYABLE = {429, 500, 502, 503, 504}
MAX_WAIT = 60  # seconds


def synthesize(payload, attempts=5):
    for attempt in range(attempts):
        response = requests.post(
            "https://api.voice213.com/api/v1/tts",
            headers={"Authorization": f"Bearer {os.environ['VOICE213_API_KEY']}"},
            json=payload,
            timeout=300,
        )
        if response.ok:
            return response.json()
        try:
            body = response.json()
        except ValueError:
            body = {}
        # A daily limit or a paused model won't clear within a few retries
        lasting = body.get("code") == "MODEL_DISABLED" or "Daily limit" in str(body.get("error", ""))
        if response.status_code not in RETRYABLE or lasting or attempt == attempts - 1:
            response.raise_for_status()
        # Honor Retry-After when it's sent (capped), otherwise back off exponentially
        time.sleep(min(float(response.headers.get("Retry-After", 2 ** (attempt + 1))), MAX_WAIT))

A 429 from your key's per-day limit won't clear within a few retries: when the message says Daily limit exceeded, the helpers above stop, and you can try again the next day. A 503 with the code MODEL_DISABLED can last a while too, so they stop on it as well: send the voice-over with another model. During maintenance, Retry-After can be long, so they wait at most a minute between attempts.

​Set generous timeouts

Generation time grows with the length of the text, and with the model: a short voice-over is ready in seconds, while a long script with a Pro model can take minutes. Voice213 doesn't cut long requests short, so set your client timeout in minutes, not seconds: 5 minutes covers long voice-overs.

​When your client gives up

If your client cancels a request or times out, Voice213 still finishes the generation, and charges it, even though you never receive the result. A retry after a timeout can therefore cost twice, which is why generous timeouts matter more than fast retries. Until it finishes, the abandoned voice-over also counts toward your 20 requests at once, and its price stays set aside from your API credit. Voice-overs in the studio format are saved to your History in the web app, so you can still download one that finished after your client stopped waiting.

​Stay within the limits

  • Requests per minute and per day: 600 a minute and 1,000 a day for every key, whatever your plan.
  • Requests at once: run at most 20 voice-overs at the same time. The next one gets 429 until one of them ends.
  • Words per voice-over: up to 5,000 in either format, tone directions included.
  • Tone directions: free, but they can't outweigh the spoken text. A text may be at most twice its billable characters plus 1,000 characters long, or it's refused with 400 TEXT_TOO_LONG.
  • Very short requests: every request is billed at least 100 characters, so batch short lines into one request when you can.
  • Long scripts: split them at sentence boundaries and join the audio afterwards. Shorter requests also come back sooner:
import re


def split_text(text, limit=3000):
    """Split text into pieces of at most `limit` characters, at sentence ends."""
    pieces, current = [], ""
    for sentence in re.split(r"(?<=[.!?؟\n])\s+", text.strip()):
        if len(current) + len(sentence) + 1 <= limit:
            current = f"{current} {sentence}" if current else sentence
            continue
        if current:
            pieces.append(current)
        while len(sentence) > limit:  # one sentence longer than the limit
            pieces.append(sentence[:limit])
            sentence = sentence[limit:]
        current = sentence
    if current:
        pieces.append(current)
    return pieces

See Rate Limits for the details.

​Keep an eye on your API credit

When your API credit can't cover a request, it fails with 402 and INSUFFICIENT_CREDIT. To avoid surprises:

  • Read remainingCredit from every paid response, or call GET /me, and raise an alert when it drops below what a busy day costs you.
  • Before a big batch, price it with POST /tts/estimate: its enough field tells you whether your credit covers it.
  • Voice-overs you run at the same time each set their price aside from your credit while they run. A batch run in parallel can therefore get 402 before any of it is charged, once the running voice-overs hold what's left.
  • Read prices from GET /models rather than hard-coding them, so a price change reaches your own price display on its own.
  • Watch your balance, spend and calls on the Developer Dashboard.
  • Top up before a launch or a campaign.

​Save what you want to keep

The same text with the same voice, dialect and model doesn't need to be generated twice. Store the audio you get back (for example in your own object storage, keyed by a hash of the request) and serve repeat requests from there. It's faster for your users and costs nothing.

Download the files behind the URLs the API returns as soon as you get them, rather than linking to Voice213's copies forever.

​Launch checklist

  • The API key lives only on your server, in an environment variable or secret manager.
  • 429, 500, 502 and 503 are retried with backoff. Other errors aren't, and a daily limit or a paused model (MODEL_DISABLED) stops the retries.
  • Client timeouts are generous (minutes, not seconds) for long scripts and Pro models.
  • Long scripts are split into shorter pieces.
  • Generated audio is stored on your side.
  • No code still calls a removed endpoint.
  • Someone gets alerted when remainingCredit runs low or a 402 comes back, and the credit is topped up.

Was this page helpful?