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, "billed_seconds": 13 }
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);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 balance. 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 uses your account's balance, the same one as the ovozAI bot and website:
- Transcription takes voice-to-text time, by the length of the recording: first the free 1 h every month, then purchased time. Timestamps take twice the length.
- Speech takes text-to-voice time, by the length of the audio made: 5 min free, then purchased time.
- Recordings with no speech, and requests that fail, cost nothing.
| Voice-to-text time | Uzbek cards | International cards |
|---|---|---|
| 1 h | 10,000 UZS | $1.49 |
| 3 h | 27,000 UZS | $2.99 |
| 7 h | 60,000 UZS | $6.49 |
| Text-to-voice time | Uzbek cards | International cards |
|---|---|---|
| 10 min | 10,000 UZS | $1.49 |
| 1 h | 54,000 UZS | $5.99 |
| 2 h | 102,000 UZS | $10.49 |
Buy time on the website. Purchased time doesn't expire. 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×): verbose_json, srt, vtt. |
timestamp_granularities[] | optional | word adds word timings to verbose_json. |
The response carries the headers OvozAI-Transcription-Id and OvozAI-Billed-Seconds. 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 or 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×). |
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
"billed_seconds": 3669, // 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 twice the recording's length from your balance, 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) or male. |
response_format | optional | mp3 (default), opus (Ogg Opus, as Telegram voice messages) or wav. |
model | optional | Any value; ignored. |
Long texts take a while: about a minute for 5,000 characters. The headers OvozAI-Audio-Seconds and OvozAI-Billed-Seconds say how long the audio is and what it took.
Balance
GET /balance:
{
"object": "balance",
"unlimited": false,
"transcription": {
"free_seconds": 2400, // free time left this month
"free_resets_at": "2026-11-01T00:00:00.000Z",
"purchased_seconds": 25200,
"available_seconds": 27000 // less what waiting jobs will take
},
"speech": { "free_seconds": 300, "purchased_seconds": 0 },
"top_up_url": "https://app.ovozai.uz/?page=buy"
}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 time left for this; the message says how much is needed. |
| 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}. 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.