Analysis

One API for text, image, video and audio

TchaviBy Gael — Tchavi founder

· 9 min read

Back to blog

Table of contents

Yes, one key and one base URL are enough for text, images, voice, transcription, embeddings, video and Jev decisions. You change the endpoint path and the model field, nothing else.

To check it, I ran a real test: a chat call, an image at minimum quality, a voice clip, embeddings, a transcription of that clip and a Jev decision. Everything went through the same key, for 26.549 credits in total, about KES 2 at October 2026 rates. I did not generate the video: I give you the flow and the price per second.

In short

  • Tchavi, an OpenAI-compatible AI API gateway you pay for with mobile money, serves six kinds of calls with a single key: chat, image, audio (voice and transcription), embeddings, video and Jev decisions. Base URL: https://tchavi.com/api/v1.
  • What changes from one type to the next: the path (/chat/completions, /images/generations, /audio/speech…) and the model. The Authorization header stays the same.
  • Video is asynchronous: you send a request, get a job ID, then poll /v1/jobs/:id until the video is ready.
  • Measured in October 2026, through Tchavi: 26.549 credits for the six calls, about KES 2 (1 credit is about KES 0.076).
  • Tchavi is not cheaper than calling each provider directly: what you gain is one key, one balance and one log, and payment in mobile money.

The principle: one header, six paths

Every call below starts from this small Node helper (version 20 or later). It reads the key from an environment variable and reads the x-credits-used header of each response.

const BASE = 'https://tchavi.com/api/v1';
const headers = { Authorization: `Bearer ${process.env.TCHAVI_API_KEY}` };

async function post(path, body) {
  const res = await fetch(`${BASE}${path}`, {
    method: 'POST',
    headers: { ...headers, 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  console.log(path, res.status, 'credits:', res.headers.get('x-credits-used'));
  return res;
}

The principles doc explains the rest: the format is OpenAI's, and every response carries X-Credits-Used, X-Credits-Remaining and X-Request-Id.

These are public prices; some models are on discount right now, and each model page shows the price billed today.

One note on the examples: the text inputs of the chat, voice, transcription, embeddings and Jev calls are the French strings I measured with, with an English gloss in the comments. The token counts and the Jev result below come from those runs; English input would count slightly differently.

Text: chat

Endpoint: POST /v1/chat/completions (doc). Test model: GPT-5 Mini, at 0.55 credits per 1,000 input tokens and 4.4 per 1,000 output tokens.

const res = await post('/chat/completions', {
  model: 'gpt-5-mini',
  messages: [
    {
      role: 'user',
      // "Explain in two sentences what an online shop in Cotonou is."
      content: "Explique en deux phrases ce qu'est une boutique en ligne à Cotonou.",
    },
  ],
  max_completion_tokens: 400,
});
const data = await res.json();
console.log(data.choices[0].message.content);

Measured: 23 input tokens, 59 output tokens, 0.273 credits. You can check the arithmetic: 23 × 0.55 / 1,000 + 59 × 4.4 / 1,000 = 0.27225, rounded up once to the next thousandth.

Image: the lowest quality

Endpoint: POST /v1/images/generations (doc). Test model: GPT Image 1 Mini, at 11 credits per image at low quality.

import { writeFileSync } from 'node:fs';

const res = await post('/images/generations', {
  model: 'gpt-image-1-mini',
  prompt: 'A stall of mangoes at a market in Nairobi, morning light',
  size: '1024x1024',
  quality: 'low',
});
const data = await res.json();
writeFileSync('mangoes.png', Buffer.from(data.data[0].b64_json, 'base64'));

Measured: 11 credits, the low quality price. If you leave out quality, the high quality is billed: 77 credits on this model. For a test, remember to set it.

Voice: from speech synthesis to transcription

Two endpoints in the audio doc. Speech synthesis bills per 1,000 characters; transcription bills per started minute.

Synthesis: POST /v1/audio/speech, with TTS Standard (tts-1) at 32 credits per 1,000 characters. The response is the raw audio file.

// "Hello, your order is ready. The delivery rider will arrive in ten minutes."
const text = 'Bonjour, votre commande est prête. Le livreur arrive dans dix minutes.';
const res = await post('/audio/speech', { model: 'tts-1', input: text, voice: 'nova' });
const mp3 = Buffer.from(await res.arrayBuffer());
writeFileSync('order.mp3', mp3);

Measured: 70 characters, 2.24 credits (70 × 32 / 1,000), an mp3 of about 69 KB.

Transcription: POST /v1/audio/transcriptions, as multipart/form-data, with Whisper at 13 credits per started minute.

const form = new FormData();
form.append('model', 'whisper-1');
form.append('language', 'fr');
form.append('file', new Blob([mp3], { type: 'audio/mpeg' }), 'order.mp3');

const res = await fetch(`${BASE}/audio/transcriptions`, {
  method: 'POST',
  headers, // no Content-Type: fetch adds the multipart boundary
  body: form,
});
console.log((await res.json()).text, res.headers.get('x-credits-used'));

Measured on the clip I had just generated: 13 credits for a measured duration of 0.07 minutes. A four-second clip is therefore billed as a full minute. The transcription was correct, with one detail: it ended with "et…" ("and…"), which was not in the original text. Always re-read the end of an automatic transcription.

Embeddings: vectors

Endpoint: POST /v1/embeddings (doc). Model: Embedding Small, at 0.044 credits per 1,000 tokens.

const res = await post('/embeddings', {
  model: 'text-embedding-3-small',
  input: 'Livraison à Cotonou', // "Delivery in Cotonou"
});
const data = await res.json();
console.log(data.data[0].embedding.length); // 1536

Measured: 6 tokens, 1,536 dimensions, 0.001 credits. The raw calculation gives 0.000264, but the total is rounded up to the next thousandth of a credit, so 0.001. On a small call that floor matters; to vectorise a catalogue, it is the large volumes that count.

Jev decisions: typed answers

Endpoint: POST /v1/decisions (doc). Instead of asking a chat model for JSON, you declare questions and receive typed answers with their probabilities. Model: Jev 1.13, at 0.09 credits per 1,000 input tokens.

const res = await post('/decisions', {
  model: 'jev-1.13',
  // "The customer writes: 'My API key stopped working since I renewed it.'"
  state: "Le client écrit : « Ma clé API ne marche plus depuis que je l'ai renouvelée. »",
  questions: {
    team: {
      type: 'choice',
      instructions: 'Quelle équipe doit traiter cela ?', // "Which team should handle this?"
      criteria: {
        billing: 'paiements ou crédits', // payments or credits
        technical: 'API, clés', // API, keys
      },
    },
    urgency: {
      type: 'score',
      instructions: "Quel est le niveau d'urgence ?", // "What is the urgency level?"
      criteria: ['pas urgent', 'normal', 'urgent', 'critique'], // not urgent, normal, urgent, critical
    },
  },
});
console.log((await res.json()).answers);

Measured: the technical team with a probability of 1, and an urgency of 1.6 on the 0 to 3 scale (confidence 0.6), for 0.035 credits. Decision models only work on this endpoint: sending jev-1.13 to /v1/chat/completions returns a 400 error.

Video: submit, then poll

Video generation is asynchronous, because a render takes time. Two endpoints: POST /v1/videos/generations (doc) and GET /v1/jobs/:id (jobs doc).

The flow:

  1. You send the request. The response comes back immediately with a 202 and a job id.
  2. You poll GET /v1/jobs/:id. The status goes from pending to processing, then to completed or failed.
  3. At completed, the response contains output.video_url. The video is deleted 24 hours after it is generated: download it.
  4. At failed, the credits charged are refunded automatically.
const submit = await post('/videos/generations', {
  model: 'veo-3.1-lite',
  prompt: 'A boda boda rider crosses a Nairobi market at sunrise',
  duration: 4,
  resolution: '720p',
  generate_audio: false,
});
const job = await submit.json();

let task;
do {
  await new Promise((r) => setTimeout(r, 5000));
  task = await (await fetch(`${BASE}/jobs/${job.id}`, { headers })).json();
} while (task.status === 'pending' || task.status === 'processing');

console.log(task.status, task.output?.video_url);

I did not generate a video for this article. The price is per second of output. For Veo 3.1 Lite, it is 66 credits per second at 720p without audio, and 110 with audio: a 4-second video without audio therefore comes to 264 credits, about KES 20. Accepted durations run from 4 to 8 seconds.

The total, call by call

CallModelMeasured creditsAbout KES
Chat (23 tokens in, 59 out)gpt-5-mini0.2730.02
Image (1024×1024, low)gpt-image-1-mini110.83
Voice (70 characters)tts-12.240.17
Embeddings (6 tokens)text-embedding-3-small0.001under 0.01
Transcription (4 s clip)whisper-1130.98
Jev decision (2 questions)jev-1.130.035under 0.01
Total26.5492.01

At about KES 0.076 per credit, that is roughly KES 2 for six calls of six different kinds. The two most expensive calls, the image and the transcription, are also the ones priced as a flat amount per unit: one image, one minute. Measured in October 2026, through Tchavi, on a test instance at public prices.

What it changes, and what it does not

What you gain: one key to protect, one balance to watch, one log where everything appears, and a single error response format. For a shop that sends a voice confirmation, a product thumbnail and a WhatsApp reply, it is one account to top up with mobile money.

What you do not gain: a token price lower than the providers', or an identical API for every model. Each model keeps its own parameters: a parameter it does not accept returns a 400 error that names it. For a general comparison, read What is an AI API gateway, and when do you need one.

FAQ

Is one key enough for text, image, voice and video?

Yes. You send the same key in the Authorization header for every endpoint. Only the path and the model field change.

How does video generation by API work?

It is asynchronous. You submit the request, receive a job ID, then poll GET /v1/jobs/:id until completed or failed. The video is deleted 24 hours after it is generated.

How much does an image, voice or transcription call cost?

It depends on the model. In October 2026, an image at low quality cost 11 credits, 70 characters of voice 2.24 credits and a transcription 13 credits. Each model's page gives the current price.

Do I have to change SDK to go from text to image?

No, if you use an OpenAI-compatible SDK with the base https://tchavi.com/api/v1. The examples in this article simply use fetch.

What happens if a video fails?

The credits charged are refunded automatically, and the job shows failed with an error code.

Can I use embeddings with the same credits?

Yes. Every call draws on the same credit balance, and each response shows its cost in X-Credits-Used.

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.