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

> Give your WhatsApp bot or app a voice with a text-to-speech API: cURL, Node, Python, the OpenAI SDK, voices, instructions and measured cost per character.

- Published: 2026-10-12
- Updated: 2026-11-02
- Author: Gael — Tchavi founder
- Canonical: https://tchavi.com/en/blog/text-to-speech-api-5-minutes

---


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](https://tchavi.com/en/docs/audio).

## 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:

```bash
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](https://tchavi.com/en/docs/audio) 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:

```javascript
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

```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:

```javascript
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`](https://tchavi.com/en/m/openai/tts-1), [`tts-1-hd`](https://tchavi.com/en/m/openai/tts-1-hd) and [`gpt-4o-mini-tts`](https://tchavi.com/en/m/openai/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:

```json
{
  "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:

```json
{
  "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](https://tchavi.com/fr/blog/chatbot-whatsapp-ia-nodejs-afrique) (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 `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

| 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](https://tchavi.com/en/docs/errors). 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](https://tchavi.com/en/docs/payments), including M-Pesa in Kenya.

[create your account and top up KES 250](https://tchavi.com/en/register?next=%2Fdashboard%2Fbilling%3Famount%3D250%26currency%3DKES%26from%3Dblog-tts-en)

> _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](https://tchavi.com/en/docs/payments)). More guides on [the blog](https://tchavi.com/en/blog). Follow us: [LinkedIn](https://www.linkedin.com/company/tchavi) and [X](https://x.com/tchaviai)._

