To give your bot or app a voice, you need one call: POST /v1/audio/speech with a text, a model and a voice. The response is a raw audio file, not JSON. You save it, play it or send it.
I made the calls for real in October 2026, through Tchavi, with cURL, Node, Python and the OpenAI SDK. I used a 131-character delivery message. I wrote it in French, because that is what I tested, so the sentences in the code below stay in French and the numbers are exactly as measured. Here is the code, what I measured, and how to check that what you receive is really audio.
Update, 26 October 2026. OpenAI shuts down
tts-1,tts-1-hdandgpt-4o-mini-ttson 6 January 2027, and Tchavi stops serving them that day. The code below works until then. After that, switch tovoxtral-tts-3(French voicefr_marie_neutral, mp3, 29 credits per 1,000 characters) orgemini-3.8-flash-lite-tts(wav or pcm, 38 credits): change the model and the voice. Details in the audio docs.
In short
- Tchavi, an OpenAI-compatible AI API gateway you pay for with mobile money, exposes
POST /v1/audio/speech: the same format as the OpenAI API, so the OpenAI SDK works once you change thebaseURLand the key. - Three text-to-speech models:
tts-1(32 credits per 1,000 characters),tts-1-hd(64) andgpt-4o-mini-tts(40), the only one that acceptsinstructionsfor tone and pace. - The bill is per character: my four 131-character clips cost 4.192, 5.24, 5.24 and 8.384 credits, exactly the per-1,000-character price pro rata. At October 2026 rates (about 13,204 credits per KES 1,000), the lowest-priced clip costs about KES 0.32.
- Each call took between 1.8 and 2.3 seconds for roughly 8 to 10 seconds of audio. The file arrives in one block; there was no progressive playback in these tests.
- Always check the
content-type: on an error, you get JSON instead of audio.
The first call, with cURL
Create a key in your dashboard, then:
curl -X POST https://tchavi.com/api/v1/audio/speech \
-H "Authorization: Bearer $TCHAVI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "tts-1",
"voice": "nova",
"input": "Bonjour Awa, ta commande est partie de Cotonou. Le livreur arrive entre quinze heures et dix-sept heures. Merci pour ta confiance !"
}' \
-D - \
-o delivery.mp3
The -D - option prints the headers. On my call I got content-type: audio/mpeg, 129,408 bytes and x-credits-used: 4.192. MP3 is the default format.
The request parameters, as the audio documentation describes them:
| Parameter | What it does |
|---|---|
model | tts-1, tts-1-hd or gpt-4o-mini-tts |
input | The text to speak: 4,096 characters at most, 2,000 for gpt-4o-mini-tts |
voice | alloy, ash, ballad, coral, echo, fable, nova, onyx, sage, shimmer, plus cedar, marin, verse for gpt-4o-mini-tts |
instructions | gpt-4o-mini-tts only: how to speak |
response_format | mp3 (default), opus, aac, flac, wav, pcm |
speed | From 0.25 to 4.0, default 1.0 |
Node.js
With fetch, no dependency:
import { writeFileSync } from 'node:fs';
const res = await fetch('https://tchavi.com/api/v1/audio/speech', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.TCHAVI_API_KEY}`,
},
body: JSON.stringify({
model: 'tts-1',
voice: 'nova',
input: 'Bonjour Awa, ta commande est partie de Cotonou.',
}),
});
// fetch does not throw on a 4xx or a 5xx: we check ourselves.
const type = res.headers.get('content-type') ?? '';
if (!res.ok || !type.startsWith('audio/')) {
throw new Error(`TTS refused (${res.status}): ${await res.text()}`);
}
const audio = Buffer.from(await res.arrayBuffer());
console.log(type, audio.length, 'bytes,', res.headers.get('x-credits-used'), 'credits');
writeFileSync('delivery.mp3', audio);
Python
import os
import requests
r = requests.post(
"https://tchavi.com/api/v1/audio/speech",
headers={"Authorization": f"Bearer {os.environ['TCHAVI_API_KEY']}"},
json={
"model": "tts-1-hd",
"voice": "onyx",
"input": "Bonjour Awa, ta commande est partie de Cotonou.",
"response_format": "mp3",
},
timeout=60,
)
r.raise_for_status()
assert r.headers["content-type"].startswith("audio/"), r.text[:200]
print(len(r.content), "bytes,", r.headers.get("x-credits-used"), "credits")
open("delivery.mp3", "wb").write(r.content)
raise_for_status() covers HTTP errors. The assert is there for the case where a proxy returns an HTML page with a 200 code.
The OpenAI SDK
If your code already uses the OpenAI SDK, you only change the address and the key. This is the version I ran, with gpt-4o-mini-tts and the marin voice:
import OpenAI from 'openai';
import { writeFileSync } from 'node:fs';
const client = new OpenAI({
apiKey: process.env.TCHAVI_API_KEY,
baseURL: 'https://tchavi.com/api/v1',
});
const res = await client.audio.speech.create({
model: 'gpt-4o-mini-tts',
voice: 'marin',
input: 'Bonjour Awa, ta commande est partie de Cotonou.',
instructions: 'Ton enjoué et rapide, comme une vendeuse qui annonce une bonne nouvelle.',
response_format: 'mp3',
});
writeFileSync('delivery.mp3', Buffer.from(await res.arrayBuffer()));
console.log(res.headers.get('content-type'), res.headers.get('x-credits-used'));
Result on my run: audio/mpeg, 145,920 bytes, 5.24 credits, 2.3 seconds.
Choosing the model and the voice
| Model | Credits per 1,000 characters | Max input | What it brings |
|---|---|---|---|
tts-1 | 32 | 4,096 | The cheapest of the three, the simplest |
tts-1-hd | 64 | 4,096 | Higher audio quality, at twice the price |
gpt-4o-mini-tts | 40 | 2,000 | instructions, plus the cedar, marin and verse voices |
These are public prices; some models are on discount right now, and each model page shows the price billed today. Find the three on their pages: tts-1, tts-1-hd and gpt-4o-mini-tts.
My advice to start: tts-1 to test, gpt-4o-mini-tts as soon as you want to set the tone. I have no objective measure of accent quality in French, so I won't claim that one voice is better than another. I also only tested French text, so I can't tell you how the voices handle other languages. Try one of your real sentences, changing only the voice, and listen on your typical customer's phone, not your headphones.
The cedar, marin and verse voices only work with gpt-4o-mini-tts. I sent marin to tts-1: the API answered 502 provider_error, a generic message that doesn't say the voice is the problem. If you see a 502 on a voice that isn't in the tts-1 list, that is probably it.
Setting the tone with instructions
gpt-4o-mini-tts accepts an instructions field, in natural language:
{
"model": "gpt-4o-mini-tts",
"voice": "coral",
"input": "Bonjour Awa, ta commande est partie de Cotonou.",
"instructions": "Voix chaleureuse de service client, rythme posé, accent français neutre."
}
With this instruction, the 131-character clip lasted about 9.8 seconds, against 8.1 seconds for tts-1 with the nova voice. The gap is real, but I can't say it comes from the instructions: the model and the voice were different. Compare with the same model and voice before drawing a rule.
If you send instructions to tts-1 or tts-1-hd, you get a clear error:
{
"error": "bad_request",
"message": "instructions parameter is only supported with steerable TTS models (gpt-4o-mini-tts)",
"statusCode": 400
}
The format for WhatsApp and the web
The format you pick changes the file size. On the text "Bonjour Awa" (11 characters, tts-1), I got:
response_format | content-type | Bytes |
|---|---|---|
| mp3 | audio/mpeg | 11,520 |
| opus | audio/opus | 5,438 |
| aac | audio/aac | 5,460 |
| flac | audio/flac | 24,517 |
| wav | audio/wav | 30,044 |
| pcm | audio/pcm | 31,800 |
All six cost the same: 0.352 credits, because the bill follows characters, not format. The opus file is an Ogg container with the Opus codec (the file command identifies it that way), roughly half the size of the MP3 on this example. That is the format messaging apps use for voice notes, but check your channel's API requirements (exact MIME type, maximum duration) before you wire it up. To build the bot itself, read the WhatsApp chatbot guide (in French).
ogg is not an accepted value for response_format: I got a 502 when I sent it. Use opus.
What it costs: billing per character
The price is given per 1,000 characters, calculated pro rata, and rounded up once to the next thousandth of a credit. My measurements, on 131 characters. KES figures are at October 2026 rates (about 13,204 credits per KES 1,000, so 1 credit is about KES 0.0757):
| Model | Calculation | Measured (x-credits-used) | In KES |
|---|---|---|---|
tts-1 | 131 × 32 / 1,000 | 4.192 | about KES 0.32 |
gpt-4o-mini-tts | 131 × 40 / 1,000 | 5.24 | about KES 0.40 |
tts-1-hd | 131 × 64 / 1,000 | 8.384 | about KES 0.63 |
The calculation comes out exact every time. For a bot, here is an order of magnitude computed from those prices: a 300-character reply with tts-1 costs 9.6 credits (about KES 0.73). A thousand replies of that size: 9,600 credits, about KES 727. Credits are your limit, not a subscription: there is none, and purchased credits never expire.
One useful safeguard:
- Cap the length in your code before calling. An
inputof 2,001 characters ongpt-4o-mini-ttsreturns400("input exceeds 2000 characters") and costs nothing, but it is better to split at sentence boundaries yourself.
The errors I ran into
| Case | Response |
|---|---|
input missing | 400 bad_request, "input is required" |
instructions on tts-1 | 400 bad_request |
Text too long for gpt-4o-mini-tts | 400 bad_request |
| Invalid key | 401 invalid_api_key |
Voice or format not supported by tts-1 | 502 provider_error |
| Too many requests | 429 rate_limit_exceeded, with Retry-After |
| Insufficient balance | 402 insufficient_credits |
I did not trigger the 402 for this article; it comes from the errors page. The 400 and 401 errors I triggered debited nothing.
FAQ
What is the character limit?
4,096 characters per request for tts-1 and tts-1-hd, 2,000 for gpt-4o-mini-tts. Beyond that, split the text at sentence ends and join the files, or send several voice messages.
Does the audio stream?
In my tests, no: the complete file arrives at once, in 1.8 to 2.3 seconds for about ten seconds of audio. I did not test progressive playback, so I promise nothing there.
What is the difference between tts-1 and tts-1-hd?
HD is billed 64 credits per 1,000 characters, twice tts-1, for higher audio quality according to the documentation. Listen to both on your sentence before paying double.
Do the voices speak French well?
They read French. I don't measure the accent, it is subjective: test on a real text, with the place and person names of your customers.
How long is the audio for a given text?
On my runs, 131 characters gave about 8 to 10 seconds depending on the voice and the model (duration estimated from the MP3 stream).
Can you clone a voice or create your own?
Not with these three models: you choose from the voices in the list.
How do I pay without a bank card?
With mobile money: the minimum top-up is KES 250, and there is no subscription and no free credits at signup. Your first paid top-up adds 150 bonus credits. See countries and operators, including M-Pesa in Kenya.
create your account and top up KES 250
This article is part of the series "Building an AI product from Africa". Tchavi is an OpenAI-compatible AI API gateway: one key gives access to over 100 text, image, audio and video models, and you pay for it with mobile money, no bank card (countries and operators). More guides on the blog. Follow us: LinkedIn and X.