Before you launch, make sure your API key stays private, your code handles errors and limits properly, and your API balance can't run dry unnoticed.
Keep your key on the server
Call MenaVoice from your backend, never from browser or mobile app code. A key shipped to a client can be extracted and used to spend your 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.menavoice.ai/api/tts", {
method: "POST",
headers: {
"x-api-key": process.env.MENAVOICE_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ text: String(req.body.text).slice(0, 5000), voiceId: "layla" }),
});
const result = await response.json();
if (!response.ok) return res.status(response.status).json({ error: result.error });
res.type(result.mimeType).send(Buffer.from(result.audio, "base64"));
});import base64
import os
import httpx
from fastapi import FastAPI, HTTPException, Response
app = FastAPI()
@app.post("/api/speak")
async def speak(payload: dict):
# Authenticate your own user here, and validate what they send
async with httpx.AsyncClient(timeout=120) as client:
response = await client.post(
"https://api.menavoice.ai/api/tts",
headers={"x-api-key": os.environ["MENAVOICE_API_KEY"]},
json={"text": str(payload.get("text", ""))[:5000], "voiceId": "layla"},
)
result = response.json()
if response.is_error:
raise HTTPException(response.status_code, result.get("error"))
return Response(base64.b64decode(result["audio"]), media_type=result["mimeType"])Put your own authentication and rate limiting in front of an endpoint like this. Otherwise anyone who finds it can spend your credit through it.
Use one key per app and environment
Create separate keys for development, staging and production, and for each app. The API Keys page shows when each key was last used, and the Developer Dashboard breaks down usage and cost by key.
If a key leaks, revoke it on the API Keys page and deploy a new one. Revoked keys stop working immediately.
Handle errors and retry safely
Every error response has the same shape, with a readable message:
{ "error": "Too many requests" }Retry only the errors that can succeed on a second attempt:
| Status | Retry? | What to do |
|---|---|---|
400 | No | Fix the request. The message says what's wrong. |
401 | No | Check the API key. |
402 | No | Top up your API credit, then retry. |
413 | No | Send a smaller file or body. |
429 | Yes | Wait Retry-After seconds if present, otherwise back off. |
500, 502 | Yes | Retry with exponential backoff. |
Failed requests aren't charged, so retrying is safe. Here's a small wrapper that retries with backoff:
import os
import time
import requests
RETRYABLE = {429, 500, 502, 503, 504}
def synthesize(payload, attempts=5):
for attempt in range(attempts):
response = requests.post(
"https://api.menavoice.ai/api/tts",
headers={"x-api-key": os.environ["MENAVOICE_API_KEY"]},
json=payload,
timeout=120,
)
if response.status_code not in RETRYABLE or attempt == attempts - 1:
response.raise_for_status()
return response.json()
# Honor Retry-After when it's sent, otherwise back off exponentially
time.sleep(float(response.headers.get("Retry-After", 2**attempt)))const RETRYABLE = new Set([429, 500, 502, 503, 504]);
async function synthesize(payload, attempts = 5) {
for (let attempt = 0; ; attempt++) {
const response = await fetch("https://api.menavoice.ai/api/tts", {
method: "POST",
headers: {
"x-api-key": process.env.MENAVOICE_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (response.ok) return response.json();
if (!RETRYABLE.has(response.status) || attempt === attempts - 1) {
const { error } = await response.json().catch(() => ({}));
throw new Error(`MenaVoice ${response.status}: ${error ?? response.statusText}`);
}
// Honor Retry-After when it's sent, otherwise back off exponentially
const wait = Number(response.headers.get("retry-after")) || 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
}
}Stay within the limits
- 30 requests per minute for each endpoint, shared by all your keys.
- Concurrent requests: 5 at a time by default, 15 once your top-ups total $100, and 50 from $1,000. Queue work on your side instead of firing everything at once.
- 5,000 characters per request. Split longer scripts at sentence boundaries and join the audio afterwards:
import re
def split_text(text, limit=5000):
"""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 piecesSee Rate Limits for the details.
Keep an eye on your balance
When your API credit can't cover a request, it fails with 402 Insufficient API balance. To avoid surprises:
- Check your balance and top-up history under API Billing.
- Watch usage and cost by service, model and key on the Developer Dashboard.
- Alert on
402responses in your own monitoring, and top up before a launch or campaign.
Cache what you can
The same text with the same voice, model and dialect doesn't need to be generated twice. Store the audio (for example in object storage, keyed by a hash of the request) and serve repeat requests from there. It's faster for your users, and cached audio costs nothing.
MenaVoice 2 varies between takes. If you want a take to stay the same, generate it once and keep it.
Set generous timeouts
Generation time grows with text length, and a multi-speaker dialogue renders every line. Allow up to two minutes for long texts and dialogues, and longer for large transcriptions, rather than relying on a short default timeout.
Launch checklist
- The API key lives only on your server, in an environment variable or secret manager.
- Each app and environment has its own key.
429,500and502are retried with backoff. Other errors aren't.- Long scripts are split to 5,000 characters or fewer.
- Repeated audio is cached.
- Someone gets alerted on
402responses, and the balance is topped up.

