Guides

Text-to-Speech API in 5 Minutes: cURL, Node, Python, OpenAI SDK

TchaviBy Gael — Tchavi founder

Published · Updated · 10 min read

Back to blog

Table of contents

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-hd and gpt-4o-mini-tts on 6 January 2027, and Tchavi stops serving them that day. The code below works until then. After that, switch to voxtral-tts-3 (French voice fr_marie_neutral, mp3, 29 credits per 1,000 characters) or gemini-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 the baseURL and the key.
  • Three text-to-speech models: tts-1 (32 credits per 1,000 characters), tts-1-hd (64) and gpt-4o-mini-tts (40), the only one that accepts instructions for 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:

ParameterWhat it does
modeltts-1, tts-1-hd or gpt-4o-mini-tts
inputThe text to speak: 4,096 characters at most, 2,000 for gpt-4o-mini-tts
voicealloy, ash, ballad, coral, echo, fable, nova, onyx, sage, shimmer, plus cedar, marin, verse for gpt-4o-mini-tts
instructionsgpt-4o-mini-tts only: how to speak
response_formatmp3 (default), opus, aac, flac, wav, pcm
speedFrom 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

ModelCredits per 1,000 charactersMax inputWhat it brings
tts-1324,096The cheapest of the three, the simplest
tts-1-hd644,096Higher audio quality, at twice the price
gpt-4o-mini-tts402,000instructions, 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_formatcontent-typeBytes
mp3audio/mpeg11,520
opusaudio/opus5,438
aacaudio/aac5,460
flacaudio/flac24,517
wavaudio/wav30,044
pcmaudio/pcm31,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):

ModelCalculationMeasured (x-credits-used)In KES
tts-1131 × 32 / 1,0004.192about KES 0.32
gpt-4o-mini-tts131 × 40 / 1,0005.24about KES 0.40
tts-1-hd131 × 64 / 1,0008.384about 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 input of 2,001 characters on gpt-4o-mini-tts returns 400 ("input exceeds 2000 characters") and costs nothing, but it is better to split at sentence boundaries yourself.

The errors I ran into

CaseResponse
input missing400 bad_request, "input is required"
instructions on tts-1400 bad_request
Text too long for gpt-4o-mini-tts400 bad_request
Invalid key401 invalid_api_key
Voice or format not supported by tts-1502 provider_error
Too many requests429 rate_limit_exceeded, with Retry-After
Insufficient balance402 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.

Share

Your first API call, paid with mobile money

No bank card. Top up with mobile money in your local currency; your first top-up adds 150 bonus credits, and credits never expire.