OvozAI API
Speech to text and text to speech for your apps · https://api.ovozai.uz/v1
Quick start
The OvozAI API turns speech into text and text into speech. It's built for Uzbek (Latin or Cyrillic) and works just as well with Russian, English and the other languages people speak in one recording: each part is written in the language it was spoken.
- Sign in at app.ovozai.uz and open API to make a key.
- Send a recording:
curl https://api.ovozai.uz/v1/audio/transcriptions \
-H "Authorization: Bearer $OVOZAI_API_KEY" \
-F file=@voice.ogg
The answer is the text:
{ "id": "6f1c…", "text": "Assalomu alaykum, …", "duration": 12.4, "credits": 12 }
The transcription endpoint works like OpenAI's, so the OpenAI libraries work with OvozAI by changing the base URL:
from openai import OpenAI
client = OpenAI(base_url="https://api.ovozai.uz/v1", api_key="ovz_…")
with open("lecture.mp3", "rb") as audio:
result = client.audio.transcriptions.create(model="ovozai-transcribe", file=audio)
print(result.text)
import OpenAI from "openai";
import fs from "node:fs";
const client = new OpenAI({ baseURL: "https://api.ovozai.uz/v1", apiKey: process.env.OVOZAI_API_KEY });
const result = await client.audio.transcriptions.create({
model: "ovozai-transcribe",
file: fs.createReadStream("voice.ogg"),
});
console.log(result.text);Start with an AI assistant
Using ChatGPT, Claude, Cursor or another coding assistant? Copy this prompt into it, add what you're building, and it has everything it needs to write the integration.
I want to use the OvozAI API in my project. OvozAI does speech to text and text to speech, built for Uzbek (Latin or Cyrillic) and also good with Russian, English and recordings that mix languages. Help me integrate it: first ask me what I'm building and in which language/framework, unless I've said so below, then write working code.
What I'm building: <describe it here, e.g. "a Telegram bot in Python that transcribes voice messages">
## The API
- Base URL: https://api.ovozai.uz/v1
- Auth: header `Authorization: Bearer ovz_…` (my key; keep it on the server, read it from an environment variable such as OVOZAI_API_KEY, never ship it to a browser or app).
- Docs: https://api.ovozai.uz/docs
### Transcribe a file and wait for the text
POST /audio/transcriptions, multipart/form-data. OpenAI-compatible: the official OpenAI SDKs work with base_url set to https://api.ovozai.uz/v1 (model: "ovozai-transcribe").
- file (required): audio or video, up to 500 MB (ogg, mp3, m4a, wav, mp4, webm…)
- language (optional): "uz-Cyrl" writes Uzbek in Cyrillic; otherwise Latin. Other languages are written as spoken.
- response_format (optional): json (default) → {"id","text","duration","credits"}; text → plain text; verbose_json, srt or vtt → with timestamps (segments; add timestamp_granularities[]=word for word timings).
Response headers: OvozAI-Transcription-Id, OvozAI-Credits. A minute of audio takes a few seconds; an hour a few minutes.
### Jobs: long recordings and YouTube links
POST /transcriptions answers at once (202) with a job.
- A file as multipart (fields: file, language, timestamps=true|false, webhook_url), or JSON {"url": "https://www.youtube.com/watch?v=…", "language": …, "webhook_url": …} for a public YouTube video up to 3 hours (no timestamps for links).
- Job: {"id","status": "queued"|"processing"|"completed"|"failed","source","title","duration","credits","text","segments","words","error": {"code","message"}|null}
- GET /transcriptions/{id} (?format=text|srt|vtt for the result as a file), GET /transcriptions (latest, ?limit=), DELETE /transcriptions/{id}. Poll every few seconds, or use the webhook. Results are kept 7 days.
- Webhook: POST to webhook_url with {"id","type": "transcription.completed"|"transcription.failed","created_at","data": <job>}. Verify the OvozAI-Signature header "t=<unix time>,v1=<hex>": v1 = HMAC-SHA256 of "<t>.<raw body>" with my webhook secret (from the API page); reject if t is older than 5 minutes. Answer 2xx within 10 seconds; failed deliveries are retried.
### Text to speech
POST /audio/speech, JSON {"input": "<20–20000 characters>", "voice": "female"|"male"|<a voice name>, "response_format": "mp3"|"opus"|"wav"}. Answers with the audio bytes (OpenAI-compatible, model "ovozai-speech"). About a minute for 5,000 characters. Headers: OvozAI-Audio-Seconds, OvozAI-Credits.
- Voices (case doesn't matter): Zephyr (female, bright), Leda (female, youthful), Achernar (female, soft), Callirrhoe (female, easygoing), Charon (male, informative), Achird (male, friendly), Algieba (male, smooth), Iapetus (male, clear). "female" is Zephyr (the default), "male" is Charon.
- Expressions: sounds go inside input in angle brackets, in English whatever the text's language, where they should be heard: <laugh>, <chuckle>, <sigh>, <gasp>, <breath>, <cough>, <cry>, <short pause>, <long pause>; <whispers> whispers to the end of the sentence. They're not read aloud. A word in CAPITALS is stressed.
### Balance
GET /balance → {"credits","available_credits","unlimited","top_up_url"}.
### Prices (credits, rounded down)
- Transcription: 1 credit per second of audio (files and YouTube links)
- With timestamps: 2 credits per second
- Text to speech: 400 credits per 1,000 characters
- No speech, failures and jobs cancelled before they start cost nothing.
### Errors and limits
Errors: HTTP status + {"error": {"code","message"}}. 400 invalid_request/file_missing/unreadable_file/unsupported_url/timestamps_unavailable, 401 unauthorized, 402 insufficient_balance (message says how many credits are needed), 404 not_found, 409 not_ready/conflict, 413 file_too_large, 429 rate_limited/too_many_jobs, 502 transcription_failed/timestamps_failed/speech_failed (retry), 503 server_busy/links_unavailable (retry later).
Limits: 120 requests a minute per key; 2 transcriptions at a time per account (up to 20 waiting); 2 speech requests at a time.
## How to write it
- Use the OpenAI SDK where it fits (transcription, speech); plain HTTP for jobs, YouTube links and balance.
- Prefer jobs with a webhook (or polling) for recordings longer than a few minutes; the synchronous endpoint is fine for voice messages.
- Handle 402 by telling the user to top up, 429 by backing off, 502/503 by retrying with backoff.
- Keep the key out of client-side code and out of the repository.
Authentication
Every request carries one of your keys in the Authorization header:
Authorization: Bearer ovz_…
A key is shown once, when it's made. Keep it on your server: anyone with it can spend your credits. You can have several (one per app, say) and revoke any of them on the API page. The base URL is https://api.ovozai.uz/v1.
Pricing
The API is paid in credits, a balance of its own, apart from your time in the OvozAI bot and website. A credit is a second of transcription:
| Credits | |
|---|---|
| Transcription (a file or a YouTube link) | 1 credit a second of audio |
Transcription with timestamps (verbose_json, SRT, VTT) | 2 credits a second of audio |
| Text to speech | 400 credits per 1,000 characters of text |
Credits are rounded down, never up: a 10.6-second recording costs 10 credits (21 with timestamps), 143 characters of speech 57. Recordings with no speech, requests that fail and jobs cancelled before they start cost nothing. Your first key comes with 1,800 credits to try it.
| Credits | Transcription | Uzbek cards | International cards |
|---|---|---|---|
| 36,000 | 10 h | 90,000 UZS | $8.99 |
| 180,000 | 50 h | 390,000 UZS | $38.99 |
| 720,000 | 200 h | 1,400,000 UZS | $138 |
Launch offer: your first credit pack comes with 2× the credits if you buy it by 9 November (Tashkent time).
Buy credits on the website; they don't expire. The API page shows what you spent, day by day, and a log of every charge and failed request. Need more volume? Write to info@ovozai.uz.
Transcribe a recording
POST /audio/transcriptions takes an audio or video file (up to 500 MB; anything ffmpeg reads: ogg, mp3, m4a, wav, mp4, webm…) as multipart/form-data and answers once it's transcribed. A minute of audio takes a few seconds; an hour, a few minutes. For long recordings you may prefer a job.
| Field | ||
|---|---|---|
file | required | The recording. |
model | optional | Any value; OvozAI picks the model. OpenAI's libraries require it. |
language | optional | uz-Cyrl writes Uzbek in Cyrillic; otherwise it's Latin. Other languages are written as spoken whatever this says. |
response_format | optional | json (default), text, or with timestamps (2 credits a second): verbose_json, srt, vtt. |
timestamp_granularities[] | optional | word adds word timings to verbose_json. |
The response carries the headers OvozAI-Transcription-Id and OvozAI-Credits (what it cost). A recording with no speech comes back with empty text and costs nothing.
If the connection drops before the transcription starts, it's cancelled and costs nothing. Once it has started, it finishes and can be fetched by its id for 7 days.
Jobs: long recordings and YouTube links
POST /transcriptions answers at once (202) with a job; you then fetch it, or get a webhook when it's done. Send either a file, as multipart/form-data:
curl https://api.ovozai.uz/v1/transcriptions \
-H "Authorization: Bearer $OVOZAI_API_KEY" \
-F file=@lecture.mp4 \
-F timestamps=true \
-F webhook_url=https://example.com/ovozai
or a YouTube video link, as JSON (public or unlisted videos up to 3 hours; nothing is downloaded on your side):
curl https://api.ovozai.uz/v1/transcriptions \
-H "Authorization: Bearer $OVOZAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://www.youtube.com/watch?v=…"}'
| Field | ||
|---|---|---|
file / url | required | The recording, or a YouTube link. |
language | optional | As above: uz-Cyrl for Cyrillic Uzbek. |
timestamps | optional | true to time the text (files only, 2 credits a second). |
webhook_url | optional | A public https:// URL to call when it's done or failed. |
A job:
{
"id": "6f1c…",
"object": "transcription",
"status": "completed", // queued, processing, completed or failed
"source": "file", // or "youtube"
"title": "lecture.mp4",
"url": null, // the YouTube link, for links
"language": "uz",
"timestamps": true,
"created_at": "2026-10-31T10:00:00.000Z",
"completed_at": "2026-10-31T10:02:41.000Z",
"duration": 1834.2, // seconds of recording
"credits": 3668, // what it cost, once completed or failed
"text": "…",
"segments": [{ "id": 0, "start": 0.42, "end": 3.1, "text": "…" }],
"words": [{ "word": "Assalomu", "start": 0.42, "end": 0.93 }],
"error": null // { "code", "message" } when failed
}
| Request | |
|---|---|
GET /transcriptions/{id} | The job. ?format=text, srt or vtt gives the result as a file instead. |
GET /transcriptions | Your latest jobs, newest first, without their text (?limit= up to 100). |
DELETE /transcriptions/{id} | Deletes a job and its result. A waiting job is cancelled and costs nothing; a running one can be deleted once it's done. |
Results are deleted 7 days after they're done.
Timestamps and subtitles
With timestamps, OvozAI places every word of the transcript on the recording: you get segments (subtitle-sized lines, at most 42 characters by 2 lines) and words, or the same as SRT or WebVTT. They take 2 credits a second of the recording, and need the recording itself, so they aren't available for YouTube links. If timing fails, the whole request fails and costs nothing.
curl https://api.ovozai.uz/v1/audio/transcriptions \
-H "Authorization: Bearer $OVOZAI_API_KEY" \
-F file=@video.mp4 -F response_format=srt > video.srtWebhooks
A job with a webhook_url gets a POST when it's completed or failed:
{
"id": "evt_…",
"type": "transcription.completed", // or "transcription.failed"
"created_at": "2026-10-31T10:02:41.000Z",
"data": { …the job, with its text… }
}
Answer with any 2xx within 10 seconds. Otherwise it's sent again after 10 s, 1 min, 10 min, 1 h (5 tries in all); you can always fetch the job yourself.
Each one is signed with your webhook secret (on the API page) in the OvozAI-Signature header: t=<unix time>,v1=<signature>, where the signature is the hex HMAC-SHA256 of <t>.<raw body>. Check it, and that t is recent, before trusting the body:
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((part) => part.split("=")));
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
return fresh && crypto.timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(expected, "hex"));
}
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(part.split("=", 1) for part in header.split(","))
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(parts["t"])) < 300
return fresh and hmac.compare_digest(parts["v1"], expected)Text to speech
POST /audio/speech reads text aloud in a natural voice (Uzbek, Russian, English and more) and answers with the audio. It works like OpenAI's speech endpoint.
curl https://api.ovozai.uz/v1/audio/speech \
-H "Authorization: Bearer $OVOZAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input": "Assalomu alaykum! Bugun havo juda yaxshi.", "voice": "female"}' \
--output speech.mp3
| Field | ||
|---|---|---|
input | required | The text, 20–20,000 characters. |
voice | optional | female (default, Zephyr), male (Charon), or any of these by name: Zephyr, Leda, Achernar, Callirrhoe, Charon, Achird, Algieba, Iapetus. Hear them on the app's Text to speech page. |
response_format | optional | mp3 (default), opus (Ogg Opus, as Telegram voice messages) or wav. |
model | optional | Any value; ignored. |
Put sounds inside input, in angle brackets, right where they should be heard: <laugh>, <chuckle>, <sigh>, <gasp>, <breath>, <cough>, <cry>, <short pause>, <long pause>. Write them in English, whatever the text's language; they aren't read aloud. <whispers> whispers to the end of the sentence, and a word in CAPITALS is stressed.
Salom! <laugh> Bugun havo juda yaxshi. <short pause> Keling, sayr qilamiz.
It costs 400 credits per 1,000 characters of input, rounded down. Long texts take a while: about a minute for 5,000 characters. The headers OvozAI-Audio-Seconds and OvozAI-Credits say how long the audio is and what it cost.
Balance
GET /balance:
{
"object": "balance",
"unlimited": false,
"credits": 34200,
"available_credits": 30600, // less what waiting and running work holds
"top_up_url": "https://app.ovozai.uz/buy/api"
}
A request is turned away with 402 when available_credits doesn't cover it: a file or video's length is known before it starts, so it's checked up front.
Errors
Errors come with an HTTP status and a body like {"error": {"code": "insufficient_balance", "message": "…"}}.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A field is missing or wrong; the message says which. |
| 400 | file_missing | No file in the file field (or no url). |
| 400 | unreadable_file | The file isn't audio or video ffmpeg can read. |
| 400 | unsupported_url | Only YouTube video links are taken. |
| 400 | video_unavailable, live_video, too_long | The video is private or deleted, live, or too long. |
| 400 | timestamps_unavailable | Timestamps were asked for a YouTube link, or a format that needs them from a job made without. |
| 400 | input_too_short, input_too_long | The text for speech is too short or too long. |
| 401 | unauthorized | The key is missing, wrong or revoked. |
| 402 | insufficient_balance | Not enough credits for this; the message says how many it needs. |
| 404 | not_found | No such job (or endpoint). |
| 409 | not_ready, conflict | The job isn't done yet, or is running. |
| 413 | file_too_large | The file is over 500 MB. |
| 429 | rate_limited, too_many_jobs | Too many requests, or too many jobs waiting. |
| 502 | transcription_failed, timestamps_failed, speech_failed | It didn't work this time. Nothing was charged; try again. |
| 503 | links_unavailable, server_busy | Try again in a little while. |
Limits
- Files up to 500 MB; YouTube videos up to 3 hours.
- 2 transcriptions at a time per account; more wait their turn, up to 20 waiting or running.
- 2 speech requests at a time per account.
- 120 requests a minute per key.
Your data
Recordings sent through the API are deleted as soon as they're transcribed, and are never kept or used to improve our models. Results are kept for 7 days so you can fetch them, then deleted; delete them sooner with DELETE /transcriptions/{id}. Transcripts of YouTube links are the exception: we keep a copy, with the link, until you delete the transcription. Text sent for speech isn't kept, nor the audio made from it. If you send other people's recordings, you need their permission, and should tell your users that OvozAI processes them. See the privacy policy and terms.