Key & quick start
The bot and API share the same credit balance.
Get an API key
- Open @sonapro_bot.
- Choose 🔌 API → Create key.
- Send
sk_user_…in every request header.
X-API-Key: sk_user_…Important: never put the key in frontend code, public GitHub or chat. Creating a new key in the bot revokes the previous one.
First synthesis
The flow is asynchronous: create a job_id, poll it and download audio after done.
API="https://api.sonapro.ai"
KEY="sk_user_…"
curl -X POST "$API/v1/tts" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"text":"Hello!","voice":"vc_xxxxxxxxxx","language":"en","model":"sona-3.6"}'
curl "$API/v1/tts/JOB_ID" -H "X-API-Key: $KEY"
curl "$API/v1/tts/JOB_ID/audio" -H "X-API-Key: $KEY" -o out.mp3Endpoints
Base URL: https://api.sonapro.ai.
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/tts | Create speech or a dialogue → job_id |
| GET | /v1/tts/{job_id} | Job status |
| GET | /v1/tts/{job_id}/audio | Download audio |
| GET | /v1/tts/{job_id}/link | Short public link |
| GET | /v1/tts/{job_id}/subtitles?format=srt|vtt|ass|json|zip | Subtitles; requires subtitles:true |
| GET | /v1/tts/{job_id}/dub | Dubbing: languages, voices and price (credits and PAYG) for a finished result |
| POST | /v1/tts/{job_id}/dub | Dub a finished result → one job per language; paid in credits or PAYG |
| POST | /v1/tts/dub-options | Dubbing languages and voices before a job exists |
| GET | /v1/tts/{job_id}/fragments | Fragments of a finished voice-over: number, text, start and end |
| POST | /v1/tts/{job_id}/fragments/{n}/quote | Price of voicing a fragment again |
| POST | /v1/tts/{job_id}/fragments/{n} | Voice a fragment again → a new whole file |
| POST | /v1/tts/dialogue/parse | Dialogue: split a script or prose into roles and lines |
| GET | /v1/tts/{job_id}/dialogue | Dialogue: who says what, and when |
| GET | /v1/tts/{job_id}/stems | Dialogue: a ZIP with a separate track per role |
| GET | /v1/tts/{job_id}/stems/link | Public link to the role tracks |
| GET/POST | /v1/dialogue/templates | Dialogue templates: list or create → dlg:N |
| GET/PATCH/DEL | /v1/dialogue/templates/{ref} | One dialogue template: read, edit or delete |
| POST | /v1/dialogue/templates/from-job/{job_id} | A template from the heroes of a finished dialogue |
| GET | /v1/voices?language=en&collection=sona_v1 | Catalog; collection: library or sona_v1 |
| POST | /v1/voices/clone | Clone a voice → tmpl:N |
| GET/POST | /v1/voices/templates | List or create a template |
| PATCH/DEL | /v1/voices/templates/{num} | Edit or delete a template |
| GET | /v1/emotions | Current emotion list |
| GET/POST | /v1/pronunciation | List or save a pronunciation rule |
| DEL | /v1/pronunciation/{term} | Delete a rule |
| GET | /v1/me | Account, tariff and balance |
| GET | /v1/activity?kind=voice&status=done&limit=50 | Account job history: what has finished and what has not (voice, images, video) |
| POST | /v1/images | Generate an image |
| POST | /v1/images/edit | Generate with image references |
| GET | /v1/images/{image_id} | Job status |
| GET | /v1/images/{image_id}/file | Download PNG/JPEG/WebP |
| POST | /v1/videos | Create a Veo 3.1 clip — from text or from 1–3 images → video_id |
| GET | /v1/videos/{video_id} | Clip status |
| GET | /v1/videos/{video_id}/file | Download MP4 |
| GET | /v1/videos?limit=20 | Account clip history |
Balance and expiry — GET /v1/me
The answer covers only the account the key belongs to. The bot's Profile shows the same figures. Dates are ISO 8601 in UTC.
| Field | Voice-over credits and tariff |
|---|---|
credits_remaining | What can be spent now: every live pack plus the starter credits. Next to it: credits_total and credits_used. |
credits_expiringcredits_expires_at | How many credits burn at the nearest deadline, and when. Only the nearest date: every pack has its own term. With no credits left in packs: 0 and null. |
tariff | Current tariff: start, pro, business, ultra or null. |
tariff_expires_at | When that tariff ends. A spent pack keeps its tariff until its term ends, so this date can be later than credits_expires_at and is set even when that one is null. After it, tariff drops to a smaller live pack or becomes null. |
jobs_limit | How many voice-overs the account may run at once. |
sub_activesub_expires_at | The Ultra subscription and its end; false and null on other tariffs. |
unlimited | true — an unlimited account: nothing is charged, so the balance never moves, and tariff_expires_at is null. |
Nano and Veo plans are in product_billing. and product_billing.; null without a plan.
| Field | Nano / Veo plan |
|---|---|
daily_limitjobs_limit | Generations per day (null on Veo, no limit) and how many at once. Several Nano plans run together, and these are their sum. |
used_todayremaining_today | Used and left in the current day. |
resets_at | When the next day starts; with several plans, the soonest one. Days count from the start of the plan, not from midnight. |
expires_at | End of the plan's current term; with several plans, the soonest. |
active_until | End of the whole unbroken paid time. Without other plans it equals expires_at. |
plans | Each plan in force right now on its own: id, label, limits, use in its own day, starts_at, expires_at. A Nano plan bought on top of an active one starts at once beside it; a Veo plan starts when the current one ends. |
product_billing. — previously purchased credits that can pay for Nano. They are already part of credits_remaining, so do not add them twice. payg — the pay-as-you-go wallet: balance_usd available, reserved_usd held for running jobs; the money does not expire. payg is absent when PAYG is off for the account.
curl https://api.sonapro.ai/v1/me -H "X-API-Key: $SONA_API_KEY"Response, shortened:
{
"credits_remaining": 400000,
"credits_expiring": 400000,
"credits_expires_at": "2026-10-27T11:37:49+00:00",
"tariff": "pro",
"tariff_expires_at": "2026-10-27T11:37:49+00:00",
"jobs_limit": 4,
"product_billing": {"products": {"nano": {"plan": {
"daily_limit": 600, "used_today": 2, "remaining_today": 598,
"resets_at": "2026-09-28T11:37:49+00:00",
"expires_at": "2026-10-27T11:37:49+00:00",
"active_until": "2026-11-26T11:37:49+00:00"}}}},
"payg": {"balance_usd": "5.000000", "reserved_usd": "0.000000"}
}Voices & templates
Each template stores its own parameters.
ID formats and pricing
vc_…Library voice1 credit / charactersona_…SONA Voice v11.5 credits / charactertmpl:NPersonal template/clone1.5 credits / characterBuild a separate first-party picker with GET /v1/voices?collection=sona_v1. Every item in the combined catalog includes collection as sona_v1 or library. Filters compose, for example ?collection=sona_v1&language=uk.
Pull the voice catalog
One request returns the whole catalog for a language — handy for building your own table of id + name + description.
curl "$API/v1/voices?language=en" \
-H "X-API-Key: $KEY"
# response (trimmed)
{
"count": 393,
"voices": [
{
"id": "vc_88664ca3a4",
"name": "Emma",
"language": "en",
"gender": "feminine",
"tags": ["Conversational"],
"description": "Approachable American female ideal for customer care and support.",
"is_pro": false,
"has_preview": true,
"collection": "library"
}
]
}One voice, multiple templates
A template stores its own speed, volume and emotion. The same voice can therefore have Neutral, Angry and Calm presets. The bot asks for them when creating a template and lets you edit them from its template card.
POST /v1/voices/templates
{"engine":"sona","voice":"sona_xxxxxxxxxxxx","name":"Taras · angry",
"speed":1.05,"volume":1.0,"emotion":"angry"}
PATCH /v1/voices/templates/7
{"emotion":"neutral","speed":1.0}A parameter sent explicitly with a synthesis request overrides the template preset for that request only.
Cloning
POST /v1/voices/clone accepts multipart fields clip, name, language, plus optional speed, volume and emotion. Use 3–60 seconds of clean speech without music, up to 16 MB; on Sona 3.6 a longer sample, 30–60 seconds, keeps the accent better.
Speech parameters
Fields accepted by POST /v1/tts. An asterisk marks required fields.
| Field | Type / range | Meaning |
|---|---|---|
text * | string | Text to synthesize |
voice * | vc_…, sona_…, tmpl:N | Voice or template |
language | auto, uk, en… | auto uses the voice language |
model | sona-3.5 / sona-3.6 | Defaults to sona-3.6. sona-3.5 applies only when the request names it; the legacy names sona-pro, sona-fast and sona-hd also run as sona-3.6. The model chosen in the bot does not change the API default. |
normalization | auto / off / locale | Sona 3.6 spoken numbers, dates, times, currencies and abbreviations; e.g. en-IN |
format | mp3 | Output format |
bitrate | 32000–192000 | MP3 bitrate; default 96000 |
sample_rate | 8000–48000 | Sample rate; default 44100 |
speed | 0.6–1.5 | SONA/clone speed |
volume | 0.5–2.0 | SONA/clone volume |
emotion | string | One delivery style for the request |
billing_source | credits / payg | Default: plan credits. Set payg explicitly for your USD wallet. No automatic fallback. |
auto_stress | bool, default true | Internal Cyrillic capital vowel → U+0301 |
subtitles | bool | SRT/VTT/ASS and JSON timings |
name | string | Filename without extension |
client_request_id | string, up to 128: Latin letters, digits, . _ : - | Your own id for the request (or the Idempotency-Key header). A retry with the same id and the same body returns the first answer with replayed: true — no second job and no second charge; the same id with another body is a 409. Kept for 72 h. Works the same on POST …/dub and POST …/fragments/{n}. |
Voice one fragment again
A finished voice-over is made of fragments of about 700 characters. One fragment can be voiced again — as it was or with corrected text (a stress mark, say). The result is a new job with a new whole file in which that fragment is replaced; the original stays as it was. It costs twice as much as an ordinary voice-over of the fragment's characters.
GET /v1/tts/{job_id}/fragments — the number (index, from 1), text, start and end of each fragment. POST …/fragments/{n}/quote — the price, with the same fields as POST /v1/tts/quote. POST …/fragments/{n} with optional text (up to 3000 characters), max_credits / max_microusd, billing_source and client_request_id returns a job_id; from there it is an ordinary job, with subtitles when the original had them. A finished voice-over's status shows fragments — how many it has. Available while the result is stored (24 h); a dub's fragment is not voiced again — fix the original and dub it again.
Template vs request emotion
Using tmpl:N without emotion applies the stored template emotion. Sending emotion:"angry" overrides it for this one request.
Pay per use (PAYG)
Top up from $5 and pay as you use speech. Sona costs $8 per million billable characters; clones and PRO voices cost $12 (×1.5). 10 concurrent tasks; funds do not expire. For example, $16 covers 2,000,000 ordinary characters.
Open /payg in Telegram to view your balance, top up and choose how the bot pays for speech. The web cabinet (api.sonapro.ai/cabinet) has its own «Оплата» (payment) switch in the speech settings; the bot's choice does not carry over to it. The API uses the same key; send billing_source:"payg" on each request. Neither the bot's choice nor the cabinet's switch changes the API default. Images and video have their own plans and the same PAYG balance: $0.015 per image, $0.02 per clip; see the Image and Video generation tabs.
GET /v1/billing/payg returns available/reserved USD and history. Create a top-up with POST /v1/billing/checkout: {"product":"payg","amount_usd":"16.25","method":"mono"} (or crypto). Payment is credited after provider verification. An empty wallet returns 402; failed/canceled speech refunds the same wallet. The TTS billing receipt reports the amount and settlement.
Dubbing
A finished voice-over in other languages, as long as the original.
How it works
The text of a finished voice-over is translated and voiced in other languages. Every language is its own job and its own file. You choose how the translation is laid: «Match the video» — each sentence starts where it is said in the original, so the track can be laid over the same video; «Natural» — like an ordinary voice-over in that language (see «Modes» below). Dubbing is open to every account, uses the same X-API-Key header and is paid in credits or from the PAYG balance (pay per use).
| Request | When |
|---|---|
GET /v1/tts/{job_id}/dub | The offer for a finished result: languages, voices and price |
POST /v1/tts/{job_id}/dub | Dub a finished result → one job per language |
POST /v1/tts + dub_languages | Order dubs together with the voice-over: the jobs are created for you once the original is done |
POST /v1/tts/dub-options | Languages and voices before the voice-over exists |
POST /v1/audio/dub-source | Dub your own recording — audio only (the «Audio dubbing» tab) |
Modes: match the video or natural
A translation is rarely as long as the original: English or Spanish often run 20–25 % shorter than Ukrainian. So choose what matters more — matching the picture or sounding natural. The field is mode in POST …/dub (dub_mode in POST /v1/tts with dub_languages); it works for ordinary voice-overs and for dialogues alike. The price is the same.
| Mode | How it sounds | When to choose it |
|---|---|---|
video«Match the video» default | Length and pauses as in the original: every phrase (in a dialogue, every line) in its place (to even out the pauses a phrase may start up to half a second earlier), the file exactly as long. A shorter translation is first slowed slightly and, if that is not enough, said more fully (no new facts); what is left is spread over the pauses — no long silence in the middle; at most a short one at the end. | The track goes under the same video: a clip, a presentation, a lesson where the sound must match the picture. |
natural«Natural» | Like an ordinary voice-over in that language: every part at a normal pace, natural pauses (no longer than ~0.9 s; in a dialogue, the dialogue's own pauses), no silence at the end. The length may differ from the original — often shorter. | A channel, a podcast, an audiobook, audio without video — where the picture does not have to match word for word. |
Without the field — video. Any other value — 422. GET …/dub and POST /v1/tts/dub-options return modes (id, name, description) and default_mode, the POST …/dub answer has mode, and a dub job's status has dub_mode. In «Natural» nothing is re-voiced, so only the base price is charged and the rest of the hold comes back.
Dub a finished result
Ask for the offer first, then start the languages you need. max_credits (max_amount_microusd for PAYG) is the most you agree to pay. Below the base price the request is refused with 409 and nothing is charged; between the base price and the maximum it caps how much re-voicing can be charged; without it the full maximum is held.
# 1. What is available and what it costs
curl https://api.sonapro.ai/v1/tts/JOB_ID/dub \
-H "X-API-Key: $SONA_API_KEY"
# 2. Start: one job per language
curl -X POST https://api.sonapro.ai/v1/tts/JOB_ID/dub \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"languages":["en","ja"],"voices":{"ja":"native"},"mode":"video","max_credits":4500}'
# → {"job_id":"JOB_ID","mode":"video","billing_source":"credits","chars":4500,"chars_base":3000,"jobs":[
# {"job_id":"DUB_ID","language":"en","status":"queued","voice":"same","chars":2250}, …]}
# 3. Every language is an ordinary speech job
curl https://api.sonapro.ai/v1/tts/DUB_ID -H "X-API-Key: $SONA_API_KEY"
curl https://api.sonapro.ai/v1/tts/DUB_ID/audio -H "X-API-Key: $SONA_API_KEY" -o dub_en.mp3In the POST …/dub response chars is the credits held now, chars_base the base price, and each job's chars in jobs its own hold. When a language is done, the base plus the re-voiced characters is charged and the rest goes back at once; a finished dub job's status shows the credits actually charged in chars.
GET …/dub response for a 1,000-character original in a catalog voice, shortened:
{
"job_id": "JOB_ID",
"source_language": "uk",
"languages": ["ar", "bg", "cs", "de", "en", "…"],
"voices": {
"en": {"auto": "native",
"native": {"id": "vc_…", "name": "…", "gender": "feminine"},
"choices": [{"id": "vc_…", "name": "…", "gender": "feminine"}, "…"],
"pro": [{"id": "sona_…", "name": "…", "pro": true, "gender": "feminine"}]}
},
"cost_per_language": 1500,
"cost_catalog_voice": 1500,
"cost_clone_voice": 2000,
"cost_per_language_max": 2250,
"cost_catalog_voice_max": 2250,
"cost_clone_voice_max": 3000,
"multiplier_catalog_voice": 1.5,
"multiplier_clone_voice": 2.0,
"billing_source": "credits",
"charge_per_language": 1500,
"charge_per_language_max": 2250,
"max_languages": 10,
"modes": [{"id": "video", "name": "Під відео", "description": "…"},
{"id": "natural", "name": "Природно", "description": "…"}],
"default_mode": "video",
"free_slots": 4,
"balance": 400000,
"payg": {
"cost_per_language_microusd": 12000,
"cost_catalog_voice_microusd": 12000,
"cost_clone_voice_microusd": 16000,
"cost_per_language_max_microusd": 18000,
"cost_catalog_voice_max_microusd": 18000,
"cost_clone_voice_max_microusd": 24000,
"balance_microusd": 5000000
}
}cost_per_language is the base price of one language in the original voice; cost_catalog_voice and cost_clone_voice apply when a language gets a catalog voice, or a clone or Sona PRO voice. The _max fields are what is held at launch: the base plus 50 % for re-voicing. multiplier_catalog_voice and multiplier_clone_voice are the price per original character. billing_source is what auto would pay with; charge_per_language and charge_per_language_max are what would be held from credits (0 for an unlimited account). payg is present only for accounts with PAYG: the same prices in micro-USD and balance_microusd. Per language, voices holds auto — what auto will pick (same or native), native — the main native-speaker voice, choices — up to 6 native voices, and pro — Sona PRO voices of that language. A dub job's status carries dub_of, the original's job_id. A finished result that can be dubbed shows dub_available: true.
Subtitles: subtitles on POST …/dub (true or false) turns them on for every language of the request; without it a dub follows the original — the GET …/dub offer shows that as subtitles, and dubs ordered through POST /v1/tts follow that voice-over's own subtitles. A finished language with subtitles shows has_subtitles: true and serves SRT, VTT, ASS or JSON from GET /v1/tts/{job_id}/subtitles, like an ordinary voice-over. No extra charge.
Pay per use (PAYG)
Dubbing can be paid from the PAYG balance: $12 per million original characters per language in a catalog voice and $16 in a clone or Sona PRO voice (the ordinary $8 PAYG rate × 1.5 and × 2), plus re-voicing at the same rate. billing_source on POST …/dub picks how to pay: auto (the default) uses PAYG when the original was paid with PAYG and PAYG is enabled for the account, credits otherwise; credits or payg choose explicitly. The price cap is max_amount_microusd, with the same rules as max_credits.
curl -X POST https://api.sonapro.ai/v1/tts/JOB_ID/dub \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"languages":["en","de"],"billing_source":"payg","max_amount_microusd":36000}'
# → {"job_id":"JOB_ID","billing_source":"payg","amount_microusd":36000,"amount_usd":"0.036000",
# "base_microusd":24000,"base_usd":"0.024000",
# "jobs":[{"job_id":"DUB_ID","language":"en",…,"amount_microusd":18000}, …]}amount_microusd and amount_usd are held now, base_microusd and base_usd are the base price, and amount_microusd on each job in jobs is its own hold. The final charge is in the billing receipt of the finished dub job's status (charged_usd). With PAYG, free_slots in the offer counts PAYG slots.
Dub while creating
Add dub_languages and, if needed, dub_voices and dub_mode (video or natural, see «Modes») to POST /v1/tts. The dub price is added to the voice-over price and held with it: POST /v1/tts/quote with the same body shows the dubs' base price in dub_credits, the hold including re-voicing in dub_credits_max, and charge_credits already includes the hold. max_credits covers the whole request: below the voice-over price plus the dubs' base price it is 409; above that it caps the re-voicing charge.
curl -X POST https://api.sonapro.ai/v1/tts \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"Hello, world!","voice":"vc_xxxxxxxxxx","language":"en","dub_languages":["de","uk"],"dub_voices":{"de":"same"},"dub_mode":"natural"}'
# → {"job_id":"JOB_ID","status":"queued","chars":13,"language":"en",
# "dub_credits":40,"dub_credits_max":60,"dub_languages":["de","uk"],"dub_mode":"natural"}
# dub progress lives in the original's status
curl https://api.sonapro.ai/v1/tts/JOB_ID -H "X-API-Key: $SONA_API_KEY"
# → … "dub_jobs":[{"language":"de","state":"started","job_id":"DUB_ID"},
# {"language":"uk","state":"waiting"}]waiting→started+job_idOnce the original is done, the service creates the dub jobs itself and each gets its own job_id. If the original fails or is canceled, everything held for the dubs is given back and the state becomes refunded.
With billing_source:"payg" the job also pays for its dub_languages from the PAYG balance: the dubs are held from the wallet together with the job and given back if the job fails or is canceled. The price cap for the whole request is then max_microusd. The quote shows dub_amount_microusd and dub_amount_max_microusd (in dollars, dub_amount_usd and dub_amount_max_usd), and amount_microusd includes the dub hold.
Voice per language
voices (or dub_voices on POST /v1/tts) maps a language to a voice. A language without an entry gets auto.
| Value | What you hear |
|---|---|
auto | The default. The original voice if it speaks that language; otherwise a native-speaker catalog voice of the same gender. A clone or a Sona PRO voice stays itself. |
same | The original voice, accent included |
native | The most popular native-speaker catalog voice, of the same gender when it is known |
vc_…, sona_…, tmpl:N | A specific voice, saved template or your own clone |
To offer the choice before a voice-over exists, send the voice, language and model to POST /v1/tts/dub-options. It returns languages, voices in the same shape and max_languages. There is no price here because it depends on the text: POST /v1/tts/quote gives it.
curl -X POST https://api.sonapro.ai/v1/tts/dub-options \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"voice":"vc_xxxxxxxxxx","language":"en","model":"sona-3.6"}'Price and limits
- Price — per character of the original, per language: 1.5 credits in a catalog voice (what a clone or Sona PRO voice-over costs), 2 credits in a clone or a Sona PRO voice. With PAYG, $12 and $16 per million original characters. Translation is included.
- Re-voicing — charged as it happens: fragments that had to be voiced again to fit the time are charged on top at the same rate (the characters of each extra take × the multiplier). At launch the base price plus 50 % of it is held (the
*_maxfields); when the dub is done, the base plus the re-voiced characters is charged and the rest of the hold goes back at once. Never more than the hold is charged — re-voicing beyond it is free. If a language's dub fails, everything held for it comes back. - Example — a 1,000-character original, one language: in a catalog voice 1,500 credits (up to 2,250 with re-voicing) or $0.012 (up to $0.018); in a clone or Sona PRO voice 2,000 (up to 3,000) or $0.016 (up to $0.024).
- Languages depend on the original's model: the 42 languages of
sona-3.5, andsona-3.6adds Odia (or) and Urdu (ur). The original's own language is not in the list. - Up to 10 languages per request. Every language is its own job and takes one of the tariff's concurrent job slots (
jobs_limit), or one of the 10 PAYG slots with PAYG. The set is admitted whole or not at all: without a free slot for every language the answer is429.free_slotsshows how many are free now. - 24 hours — how long a result is stored and can be dubbed; after that
410. A dub cannot be dubbed again — dub the original.
Errors
Nothing is charged on a refusal.
402 | Not enough credits (the message suggests paying with PAYG) or not enough PAYG funds |
403 | Dubbing is not available for this account, or PAYG is disabled for the account |
404 | Job not found |
409 | The voice-over is not finished yet, it is already a dub, this voice-over cannot be dubbed, or max_credits / max_amount_microusd is below the base price |
410 | The result has already been deleted after its storage period |
422 | Language unavailable or the same as the original's, too many languages, unknown voice, mode other than video or natural |
429 | Not enough free slots for every language |
503 | Dubbing is temporarily unavailable or maintenance is under way |
Dub your own recording
Your own audio recording or voice message can be dubbed into other languages too — in the bot or through the API. A step-by-step guide, the price and the limits are on the «Audio dubbing» tab.
Audio dubbing
Your own recording — the same track in other languages: the service recognises the text, translates it and voices it in time with the original.
How it works
- What to send — an audio file (mp3, m4a, ogg, wav, flac…) or a voice message: one voice in the recording, up to 2 h. Videos are not accepted — sound only.
- What the service does — recognises the language, the text and the time of every word, free of charge, and tells the speaker's gender. You can correct the text before dubbing: names, terms, misheard words.
- What you get — an mp3 in every chosen language. «🎬 Match the video» keeps every phrase where it was and the original's length: the track can go over the same video. «🎙 Natural» sounds like an ordinary voice-over in that language. Background music and noise are not carried over: only the new voice is heard in the result.
In the bot, step by step
- Open @sonapro_bot and send an audio file or a voice message — up to 20 MB (Telegram gives bots nothing larger).
- The bot shows the recording's card with its length and price — press «🌐 Dub this audio».
- Wait for the recognition — about a minute for every 10 minutes of recording. The bot sends the recognised text.
- If needed — «✏️ Correct the text»: copy the text, fix it and send it as the next message (up to 3,500 characters in the bot; a longer text is corrected through the API).
- «🌐 Choose languages» — tick the languages and press «Next › voices». For every language — «Auto» (a native speaker of the speaker's gender), another native voice (🔊 — listen), a Sona PRO voice 💎 or your own saved voice ⭐.
- Choose «🎬 Match the video» or «🎙 Natural», «Subtitles» if needed, and press «▶️ Start» — the button already shows the price. The finished file in every language arrives in the chat.
Via the API, price and limits
- Through the API — send the file, wait for
status: done, correct the text if needed and start the dub with the usualPOST …/dub.
| Request | What it does |
|---|---|
POST /v1/audio/dub-source | Multipart: file, optionally language (uk, en, pt-BR… or auto) and name. Answers 202: job_id, status, duration |
GET /v1/tts/{job_id} | The recording's status: source: "upload", language, gender (the speaker's, or null), transcript_chars, dub_available; while the service is busy, waiting and retry_at |
GET /v1/tts/{job_id}/transcript | The recognised text and its sentences with their times (sentences) |
PUT /v1/tts/{job_id}/transcript | Correct the text before dubbing: unchanged words keep their time in the recording |
GET / POST /v1/tts/{job_id}/dub | The offer and the launch, as for any voice-over. A recording has no Sona voice, so there is no same; auto is the most popular native voice of the speaker's gender |
# 1. Send the recording (recognition is free)
curl -X POST https://api.sonapro.ai/v1/audio/dub-source \
-H "X-API-Key: $SONA_API_KEY" \
-F file=@talk.mp3 -F language=en
# → 202 {"job_id":"JOB_ID","status":"queued","duration":512.4}
# 2. Wait for "status": "done"
curl https://api.sonapro.ai/v1/tts/JOB_ID -H "X-API-Key: $SONA_API_KEY"
# 3. The text with its sentences' times — and a correction if needed
curl https://api.sonapro.ai/v1/tts/JOB_ID/transcript -H "X-API-Key: $SONA_API_KEY"
curl -X PUT https://api.sonapro.ai/v1/tts/JOB_ID/transcript \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"Hello everyone. Today we talk about the weather."}'
# 4. The dub — as for any voice-over
curl -X POST https://api.sonapro.ai/v1/tts/JOB_ID/dub \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"languages":["uk","de"],"mode":"video"}'- Price — recognising and correcting the text are free. A recording's dub costs per character of the recognised text, per language, twice a voice-over: a catalog voice 2 credits, a clone or Sona PRO voice 3. With PAYG, $16 and $24 per million characters. Re-voicing is charged as it happens at the same rate; the base plus 50 % is held at launch, as for any dub.
- Example — a recording with 1 000 characters of text, one language: a catalog voice 2 000 credits (up to 3 000) or $0.016 (up to $0.024); a clone or Sona PRO voice 3 000 (up to 4 500) or $0.024 (up to $0.036).
- Limits — files up to 95 MB, recordings up to 2 h, up to 6 h of recordings a day per account, one recording recognised at a time. «Match the video» gives a file as long as the recording. The file itself is deleted once it is recognised; the text and the mp3 are kept for 24 hours.
411 | No Content-Length header — send the file as an ordinary request |
413 | The file is larger than 95 MB |
422 | The recording is too long (over 2 h), it is not audio or it is a video, unknown recording language, the corrected text's length differs from the recognised one by more than 30 %, or same for a recording's dub |
429 | The previous recording is still being recognised, the 6 h a day are used up, or the plan's free slots |
Dialogues
Several voices in one voice-over: every role speaks in its own voice — with pauses, overlaps and interruptions.
How it works
A dialogue is roles and lines in the order they are spoken. Every line is voiced in its role's voice, and all of them are put together into one file. We trim the silence at the start and end of every line, so a pause between lines sounds exactly as long as you set it. The roles' loudness is levelled: a quiet role is not drowned out by a loud one, and a whisper stays a whisper. Lines can overlap, sound at the same time or interrupt the previous one — an interrupted line fades out where it was cut, and an overlap does not overload the sound. Subtitles name the speaker, and every role can be downloaded as its own track.
| Request | What it does |
|---|---|
POST /v1/tts + speakers, segments | Voice a dialogue → job_id; POST /v1/tts/quote with the same body shows the price first |
POST /v1/tts/dialogue/parse | Split a script or plain prose into roles and lines — free |
GET /v1/tts/{job_id}/dialogue | A finished dialogue: who says what, and when |
GET /v1/tts/{job_id}/stems | A ZIP with a separate track per role |
GET /v1/dialogue/templates | Dialogue templates dlg:N: heroes and their voices |
POST /v1/tts/{job_id}/dub + role_voices | Dub a dialogue into other languages |
Dialogues are enabled per account: GET /v1/me returns "dialogue": true when they can be used. Until then, dialogue requests answer 403.
In the bot
- Send a script as text or as a .txt / .docx file — every line on a new row:
Name: text. The bot recognises the dialogue and opens the roles screen «🎭 Dialogue: N roles · M lines». - The roles screen has a button per role «Name — Voice». The first role gets your current voice, the others get suggested voices so the roles do not sound alike. Tap a role to choose its voice: saved voices, clones, recent voices, search by name, an ID or the catalog.
- Next › appears once every role has a voice: the card shows the roles, voices, characters and the price, and Voice it starts the dialogue. Voice it in one voice goes back to an ordinary voice-over of the same text, How to write a dialogue shows the format and the commands.
- 🎭 Split into roles — a button under an ordinary text without names: the story is split into the narrator and the characters, and the words of every line are taken from your text verbatim.
- 🎭 Dialogue templates — saved casts of heroes with their voices. A template whose heroes match the script's names is applied by itself: a role is marked «from the template» or «as last time»; 🎭 Template… picks another template or none.
- When a script has a mistake, the bot shows the reason with its row number («Рядок N: …») and never reads the names or the commands aloud.
Under a finished dialogue:
| 🎚 Role tracks | A ZIP with a separate track per role; a link to it when it is over 50 MB |
| ✂️ Voice a fragment again | The lines as a list «Name: text»: any of them can be voiced again — as it was or with corrected text |
| 🌐 Create dubbing | The dialogue in other languages: you choose only the languages, every role's voice for every language is picked for you |
| 💾 Save as a template | The heroes of this dialogue, with their voices, speed and emotion, become a new template |
Script format
A script is written the same way in the bot and for POST /v1/tts/dialogue/parse.
- A line starts on a new row:
Name: textor[Name]: text. A row without a name continues the previous line. - A remark in parentheses after the name is not voiced:
Andriy (whispering): …is the role «Andriy». - Text before the first name is read by the role «Narrator».
- Commands go in curly braces anywhere in a line; they are not voiced and not charged. A pause belongs to the end of its line, the others to its start, counted from the previous line. Numbers are seconds (a decimal comma works too); with
msorмс— milliseconds.
| Command | Ukrainian · Russian | What it does |
|---|---|---|
{pause: 1.5} | {пауза: 1.5} in both | A pause after this line, up to 30 s |
{overlap: 0.7} | {накладання: 0.7}{наложение: 0.7} | Start 0.7 s before the previous line ends |
{interrupt: 0.7} | {перебити: 0.7}{перебить: 0.7} | The same — and cut the previous line |
{start after: 2} | {почати через: 2}{начать через: 2} | Start 2 s after the previous line starts |
{interrupt after: 2} | {перебити через: 2}{перебить через: 2} | The same — and cut the previous line |
{after words: "phrase"} | {почати після: "фраза"}{начать после: "фраза"} | Start as soon as the previous line says the phrase; it must occur there exactly once |
{interrupt after words: "phrase"} | {перебити після: "фраза"}{перебить после: "фраза"} | The same — and cut the previous line |
{simultaneous} | {одночасно}{одновременно} | Together with the previous line |
Author: A forest clearing. Evening mist creeps over the lake.
Vasyl: Andriy, do you hear that? Someone is singing over the water. {pause: 1}
Andriy (whispering): I hear it. They say it is Mavka, and she lures fishermen into the deep.
Mavka: {interrupt after words: "lures fishermen"} Not true!
I just love to sing when the moon rises.
Vasyl: {overlap: 0.4} Oh... She heard us.
Author: The boys looked at each other and silently headed home.Four roles, seven rows — six lines: a second of silence after Vasyl's question, Mavka interrupts Andriy on the words «lures fishermen», the row without a name continues her line, and Vasyl starts before she has finished.
API: voice a dialogue
The same POST /v1/tts, but with roles (speakers) and lines (segments) instead of text and voice. The other fields — model, language, format, subtitles, billing_source, max_credits, name — work as for an ordinary voice-over; the request's speed, volume and emotion apply to roles without their own. From there it is an ordinary job: status, /audio, subtitles.
curl -X POST https://api.sonapro.ai/v1/tts \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"sona-3.6","subtitles":true,"dialogue_pause_seconds":0.35,
"speakers":[
{"id":"author","name":"Author","voice":"vc_xxxxxxxxxx"},
{"id":"vasyl","name":"Vasyl","voice":"tmpl:3","speed":1.05},
{"id":"mavka","name":"Mavka","voice":"sona_xxxxxxxxxxxx","volume":0.9,"emotion":"calm"}],
"segments":[
{"speaker_id":"author","text":"Mist creeps over the lake.","pause_after_seconds":1},
{"speaker_id":"vasyl","text":"Listen. Someone is singing over the water. It must be Mavka."},
{"speaker_id":"mavka","text":"Yes, it is me!","start_mode":"after_words",
"start_after_words":"over the water","interrupt_previous":true},
{"speaker_id":"vasyl","text":"She heard us.","start_mode":"overlap","start_offset_seconds":0.3}]}'
# → {"job_id":"JOB_ID","status":"queued","chars":226,"language":"en",
# "dialogue":{"speakers":3,"segments":4}}
# with the template dlg:3: the heroes' voices come from it by name, no speakers needed
curl -X POST https://api.sonapro.ai/v1/tts \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template":"dlg:3","segments":[{"speaker_id":"Vasyl","text":"Do you hear that?"},{"speaker_id":"Mavka","text":"I do."}]}'| Field | Description |
|---|---|
speakers | 2–20 roles (table below). Optional with template: then they only override the template's heroes or add new ones |
segments * | Up to 2,000 lines in the order they are spoken (table below) |
dialogue_pause_seconds | The pause between lines that set none of their own, 0–30 s; 0.35 by default |
template | dlg:N — a dialogue template: every role gets the voice of the hero with the same name |
text | Not needed. When sent, it must equal the lines' texts joined by a blank line |
| speakers[] | Description |
|---|---|
id * | Unique within the request, up to 80 characters; lines refer to it |
name | The name in subtitles, tracks and templates; defaults to id |
voice * | vc_…, sona_… or tmpl:N |
speed | 0.6–1.5 (or 60–150 %) |
volume | 0.5–2.0 (or 50–200 %): an exact gain for the role in the mix, after the loudness is levelled |
emotion | The role's emotion |
language | The role's language — roles of one dialogue may speak different languages. Defaults to the request's language; auto takes the language of the role's voice and, when that is unknown, detects it from the role's lines |
Precedence: the role's field → the request's field → the preset of a saved voice tmpl:N. Stress marks, pronunciation rules and inline tags work in every line exactly as in an ordinary voice-over.
| segments[] | Description |
|---|---|
speaker_id * | The role's id |
text * | The line's text |
pause_after_seconds | The pause after the line, 0–30 s; replaces dialogue_pause_seconds |
start_mode | When the line starts — table below; after by default |
start_offset_seconds | For overlap and after_seconds, 0–60 s |
start_after_words | For after_words: a phrase of up to 200 characters that occurs exactly once in the previous line |
interrupt_previous | true — cut the previous line where this one starts (with any start_mode but after) |
interrupt_overlap_seconds | How long both voices sound together before the cut, 0–0.5 s; 0.05 by default |
| start_mode | When the line starts |
|---|---|
after | The default: once every previous line has ended, plus the pause |
simultaneous | Together with the previous line |
overlap | start_offset_seconds before the previous line ends |
after_seconds | start_offset_seconds after the previous line starts |
after_words | As soon as the previous line says start_after_words |
A start rule counts from the previous line and replaces the pause before it. The first line is always after, and a role never overlaps itself. The POST /v1/tts response adds dialogue — how many roles and lines; the quote adds dialogue with the roles (id, name, voice, chars), the number of lines and credit_multiplier.
API: the result
A dialogue's status carries dialogue — the roles (id, name) and the number of lines. A finished one adds dialogue_url, stems_url and fragments — the number of lines that can be voiced again.
# 1. Status
curl https://api.sonapro.ai/v1/tts/JOB_ID -H "X-API-Key: $SONA_API_KEY"
# → … "dialogue":{"speakers":[{"id":"author","name":"Author"}, …],"segments":4},
# "dialogue_url":"/v1/tts/JOB_ID/dialogue","stems_url":"/v1/tts/JOB_ID/stems","fragments":4
# 2. Who says what, and when
curl https://api.sonapro.ai/v1/tts/JOB_ID/dialogue -H "X-API-Key: $SONA_API_KEY"
# 3. Role tracks: the ZIP or a public link to it
curl https://api.sonapro.ai/v1/tts/JOB_ID/stems -H "X-API-Key: $SONA_API_KEY" -o stems.zip
curl https://api.sonapro.ai/v1/tts/JOB_ID/stems/link -H "X-API-Key: $SONA_API_KEY"
# → {"url":"https://api.sonapro.ai/ds/…","expires":1791460800}
# 4. Voice the third line again, with corrected text
curl -X POST https://api.sonapro.ai/v1/tts/JOB_ID/fragments/3 \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"Yes, it is me! And I hear everything."}'GET …/dialogue response, shortened:
{
"job_id": "JOB_ID",
"duration": 7.41,
"speakers": [{"id": "author", "name": "Author", "voice": "vc_xxxxxxxxxx"}, "…"],
"segments": [
{"index": 2, "speaker_id": "vasyl", "speaker_name": "Vasyl",
"text": "Listen. Someone is singing over the water. It must be Mavka.",
"start": 3.12, "end": 5.06, "interrupted": true},
{"index": 3, "speaker_id": "mavka", "speaker_name": "Mavka",
"text": "Yes, it is me!", "start": 5.01, "end": 6.2, "interrupted": false},
"…"
]
}- Role tracks —
GET …/stemsreturns a ZIP: a separate track per role, as long as the whole dialogue, plustimeline.json.GET …/stems/linkis a public link that needs no key (urlandexpires), handy for big files. - Subtitles (
subtitles:true) name the speaker: SRT — «Name: …», simultaneous speech — one block «- A: … / - B: …», VTT —<v Name>, ASS — the Name field. - Lines as fragments —
GET /v1/tts/{job_id}/fragmentsreturns the lines withspeaker_idandspeaker_name.POST …/fragments/{n}with an optional correctedtextcreates a new job — the whole dialogue with that line voiced again; the original does not change.POST …/fragments/{n}/quoteshows the price first.
API: parsing a script
POST /v1/tts/dialogue/parse turns a text into roles and lines in the POST /v1/tts format and suggests a voice for every role. Free: up to 30 requests a minute. An ordinary text without names answers {"is_dialogue": false}; with "auto": true it is split into the narrator and the characters — up to 20,000 characters, up to 6 requests a minute; the words of every line are taken from the original verbatim.
# a script; voice is your current voice, the first role gets it
curl -X POST https://api.sonapro.ai/v1/tts/dialogue/parse \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"Vasyl: Do you hear that? {pause: 1}\nMavka: I do!","voice":"vc_xxxxxxxxxx"}'
# ordinary prose → the narrator and the characters, no template
curl -X POST https://api.sonapro.ai/v1/tts/dialogue/parse \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"Vasyl looked round and called out: “Who is there?” From the mist, Mavka answered quietly: “Me.”","auto":true,"template":"none"}'The answer to the first request, shortened:
{
"is_dialogue": true,
"auto": false,
"speakers": [
{"id": "Vasyl", "name": "Vasyl", "lines": 1, "chars": 17,
"suggested_voice": {"id": "vc_xxxxxxxxxx", "name": "…", "gender": "masculine"},
"voice_source": "current", "template_voice": null, "remembered_voice": null},
{"id": "Mavka", "name": "Mavka", "lines": 1, "chars": 5,
"suggested_voice": {"id": "vc_…", "name": "…", "gender": "feminine"},
"voice_source": "suggested", "template_voice": null, "remembered_voice": null}
],
"segments": [
{"speaker_id": "Vasyl", "text": "Do you hear that?", "pause_after": 1.0, "start_mode": "after", "…": "…"},
{"speaker_id": "Mavka", "text": "I do!", "pause_after": null, "start_mode": "after", "…": "…"}
],
"characters": 22,
"credit_multiplier": 2.0,
"language": "en",
"template": null
}- Request fields:
text(required),auto,voice— your current voice (vc_…,sona_…,tmpl:N; an unknown one is simply ignored),template,language,model. suggested_voice— the voice a role starts with, in this order: the template's hero → the voice this hero had in your last dialogue → the request'svoice(the first role only) → a catalog voice no other role has.voice_sourcesays where it came from:template,memory,current,suggestedornull. A template or memory voice comes with itsspeedandemotion; those voices themselves are intemplate_voiceandremembered_voice.template:"dlg:N"— that template,"none"— no template; without the field, your template whose heroes cover the most roles of the script (at least two, or all of them) is applied. The response then hastemplate—id,name,matchedandmissing(roles the template gave no voice), otherwisenull.segmentscan go straight intoPOST /v1/ttsonce every role has a voice. Their fields use the short names —pause_after,start_offset,interrupt,interrupt_overlap;POST /v1/ttsaccepts both spellings.- A script that could not be voiced (a broken command, too many roles or lines, too long) gets
422with the reason «Рядок N: …» (line N) right here.
Dialogue templates
A template is your saved cast: the heroes as they are named in your scripts and the voice of each (vc_…, sona_… or tmpl:N), optionally with speed, volume, emotion and language. Its reference is dlg:N: a number within the account, never reused after a delete. Up to 50 templates, 1–20 heroes in each, a name of up to 60 characters.
| Request | What it does |
|---|---|
GET /v1/dialogue/templates | The list: templates, the most recently changed first, and max |
POST /v1/dialogue/templates | Create → 201; heroes as a roles list or as text lines |
GET /v1/dialogue/templates/{ref} | One template; ref is dlg:N or N |
PATCH /v1/dialogue/templates/{ref} | Change name, roles or text — the heroes are replaced as a whole |
DELETE /v1/dialogue/templates/{ref} | Delete → {"deleted": "dlg:3"} |
POST /v1/dialogue/templates/from-job/{job_id} | A template from the heroes of a finished dialogue → 201; works even after the audio has been deleted |
# create from a list of heroes: one hero per line
curl -X POST https://api.sonapro.ai/v1/dialogue/templates \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Forest Song","text":"Author vc_xxxxxxxxxx\nVasyl tmpl:3 1.1\nAndriy: vc_yyyyyyyyyy calm\nMavka = sona_xxxxxxxxxxxx 0.9 sad"}'
# → 201 {"id":"dlg:3","num":3,"name":"Forest Song","roles":[
# {"name":"Author","voice":"vc_xxxxxxxxxx","voice_name":"…","speed":null,…}, …],…}
# or from the heroes of a finished dialogue
curl -X POST https://api.sonapro.ai/v1/dialogue/templates/from-job/JOB_ID \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Forest Song"}'
# rename
curl -X PATCH https://api.sonapro.ai/v1/dialogue/templates/dlg:3 \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Mavka and the fishermen"}'- Hero lines —
Name ID [speed] [emotion]: a space,:,-,—or=between the name and the voice; speed is 0.6–1.5 or 60–150 %. A line that cannot be read gets422«Рядок N: …» (line N). - Matching by name ignores case and extra spaces, «ё» = «е», and every apostrophe is the same.
POST /v1/ttswith"template": "dlg:3"gives every role the voice of the hero with the same name, while subtitles keep the script's spelling;speakerswith the sameidoverride a hero or add a new one. A role found in neither gets422naming it. Parsing a script applies the matching template by itself. - Remembered heroes — even without a template the service remembers the last voice of every hero by name (up to 500 per account): every accepted dialogue records it, and the next parse suggests it again (
voice_source: "memory"). - When a saved voice
tmpl:Nis deleted, the heroes that used it are left without a voice ("voice": null): the parse lists them inmissing, and they need a new one.
Dubbing a dialogue
A finished dialogue is dubbed like an ordinary voice-over — GET and POST /v1/tts/{job_id}/dub — but the voice is set for every role. Every line is translated with the previous lines and who says them in mind, voiced in its role's voice and placed where it sounded in the original; an interrupted line is cut at the same moment. Every language's file is as long as the original and is a dialogue too: lines, role tracks, subtitles, voicing a line again. A dub cannot be dubbed again.
# 1. Languages, role voices and the price
curl https://api.sonapro.ai/v1/tts/JOB_ID/dub -H "X-API-Key: $SONA_API_KEY"
# → … "dialogue":true,"roles":[{"id":"vasyl","name":"Vasyl","voice":"tmpl:3"}, …],
# "role_voices":{"uk":{"vasyl":{"auto":{"id":"tmpl:3","name":"…"},"choices":[…]}, …}, …}
# 2. Start: a voice is set only for Mavka in Ukrainian, the rest are auto
curl -X POST https://api.sonapro.ai/v1/tts/JOB_ID/dub \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"languages":["uk","pl"],"role_voices":{"uk":{"mavka":"vc_xxxxxxxxxx","author":"native"}}}'role_voices— «language → role → voice»:auto,same,native,vc_…,sona_…ortmpl:N; a role without an entry getsauto.voicesdoes not apply to a dialogue —422.auto: a clone and a Sona PRO voice stay themselves, and so does a voice that speaks the language; otherwise the role gets a native-speaker voice of the same gender, and two roles never get one voice.- The offer
GET …/dubof a dialogue addsdialogue: true,rolesandrole_voices— for every language and role, whatautopicks and up to 6 native voices inchoices. mode— as on the Dubbing tab:video(default) — every line starts where it did in the original (up to half a second earlier to even out the pauses), the file is as long, for the same video;natural— the lines follow one another with natural pauses (overlaps and interruptions are kept), the length may differ.- The price does not depend on the voices (below, «Price and limits»); languages, up to 10 languages per request, slots, holds and
max_creditswork as in the «Dubbing» tab. A dub cannot be ordered while the dialogue is created (dub_languages) — only from the finished result.
Price and limits
- A dialogue — 2 credits per character of the lines, whatever voices speak them; names, commands, settings and pauses are free. With PAYG (
billing_source:"payg") — $16 per million characters of the lines. For example, 1,000 characters of lines cost 2,000 credits or $0.016. - One line again — ×2 its characters: 2 credits per character, $16 per million with PAYG.
- Dubbing a dialogue — 2 credits per character of the original's lines per language, $16 per million per language with PAYG. Lines that had to be voiced again to fit the time are charged on top at the same rate: the base plus up to 50 % is held at launch, and what is not used goes back at once.
- Roles — from 2 to 20; an
idand a name of up to 80 characters. Lines — up to 2,000, up to 200,000 characters together. - Pauses — up to 30 s, offsets (
start_offset_secondsand the numbers in commands) — up to 60 s. The whole dialogue including pauses — up to 5 hours; a longer one gets422before anything is charged. - One job — a dialogue takes one concurrent job slot, however many roles it has.
- 24 hours — how long the file, the role tracks and the lines are kept for voicing again and dubbing; after that
410. A template can still be saved from a finished dialogue later.
Errors
Nothing is charged on a refusal.
400 | The request body could not be read — for example, it is not UTF-8 |
402 | Not enough credits or PAYG funds |
403 | Dialogues may not be enabled for this account yet (GET /v1/me → "dialogue": false), or PAYG is disabled for the account |
404 | Job or template not found, the job is not a dialogue, or there is no line with that number |
409 | The dialogue is not finished yet, the price is above max_credits / max_microusd, 50 templates are already saved, or it is already a dub |
410 | The dialogue's files have already been deleted after their storage period |
422 | A mistake in the roles, lines or script, with its place: «Роль …» (role), «Репліка N: …» (line), «Рядок N: …» (script row); an unknown voice, a role language that is not available, a dialogue that is too long, a hero missing from the template; voice, accent or dub_languages in a dialogue request; an automatic split that cannot be trusted |
429 | Too many requests (parsing — 30 a minute, with auto — 6) or every concurrent job slot is busy |
503 | Dialogues or the automatic split are temporarily unavailable, or maintenance is under way |
Stress, pronunciation & emotions
A clone carries timbre; the model still reads the text. Pronunciation is therefore controlled through text.
Three levels of stress control
| Level | Example | When to use |
|---|---|---|
| 1. U+0301 mark | за́мок / замо́к | First hint for an ambiguous word |
| 2. Capital vowel | зАмок / замОк | Only shorthand: SONA normalizes it to the same U+0301 |
| 3. Phonetic replacement | Тарас → та-РАС | Only after both hints fail and the replacement passes a listening test |
U+0301 and a capital vowel are not independent fallbacks. The latter is merely converted to the former, so a model may ignore both.
Personal SONA dictionary: Cyrillic behavior
This is not a complete pronunciation lexicon or an automatic stress detector. It is an account's list of explicit exceptions: before synthesis SONA finds term in the transcript and literally substitutes replacement. You do not pass a dictionary ID to POST /v1/tts; saved rules are applied automatically.
| Property | SONA behavior |
|---|---|
| Cyrillic | Supported in both term and replacement. Sounds-like guidance may use ordinary Ukrainian or Russian letters. |
| Case | By default, Тарас matches Тарас, тарас and ТАРАС. |
| Boundaries | Only a complete word or complete phrase matches. Тарас does not modify Тараса or Тарасові. |
| Inflections | Save each required word form separately: Тараса → та-РА-са, Тарасові → та-РА-со-ві. These demonstrate the format; listen-test every replacement. |
| Scope | Rules belong to the account, not one voice. They apply in the bot and API to all of its voices and templates. |
| Order | Longer phrases run before shorter words. The limit is 50 rules; term is limited to 60 and replacement to 160 characters. |
Case-sensitive exceptions: the default is case_sensitive: false, preserving existing rules. With case_sensitive: true, US does not match us, and LaTeX does not match latex. Lowercase keys also accept initial capitalization: cat → cat / Cat, but not CAT. Supported by Sona 3.5 and 3.6. Example for POST /v1/pronunciation: {"term":"US","replacement":"United States","case_sensitive":true}. Overlapping spellings return 422; edit or delete the existing rule first. Use the exact term returned by the list endpoint to update or delete a particular variant.
Keep a dictionary term in its ordinary spelling inside the script. Do not also add U+0301 or a capital-vowel stress mark: the dictionary runs after stress normalization, so that altered spelling no longer equals the saved term. The substituted text is what synthesis, subtitles and character accounting receive.
# add or update one rule
curl -X POST https://api.sonapro.ai/v1/pronunciation \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"term":"Тарас","replacement":"та-РАС"}'
# save inflected forms as separate rules
curl -X POST https://api.sonapro.ai/v1/pronunciation \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"term":"Тараса","replacement":"та-РА-са"}'
# list every account rule
curl https://api.sonapro.ai/v1/pronunciation \
-H "X-API-Key: $SONA_API_KEY"
# delete a rule; percent-encode Cyrillic in the URL
curl -X DELETE https://api.sonapro.ai/v1/pronunciation/%D0%A2%D0%B0%D1%80%D0%B0%D1%81 \
-H "X-API-Key: $SONA_API_KEY"Use it for names, brands, abbreviations and terms that are consistently mispronounced. If U+0301 solves a stress-only issue, a dictionary rule is unnecessary. Sounds-like spelling guides rather than guarantees pronunciation, so test a short take in the intended language first.
Verified rule: Тарас
For the Ukrainian Taras voice, the model said ТАрас and ignored the stress mark. The listen-tested та-РАС replacement produced the intended ТарАс.
curl -X POST https://api.sonapro.ai/v1/pronunciation \
-H "X-API-Key: $SONA_API_KEY" -H "Content-Type: application/json" \
-d '{"term":"Тарас","replacement":"та-РАС"}'The rule matches a whole word, case-insensitively, for every synthesis on this API account. Never syllabify an entire script or invent IPA; listen-test each exception first.
Prompt for Claude / ChatGPT
Put this prompt before a script. The AI will inspect the whole text—not only the word “Тарас”—and return SONA-ready text.
You edit text for SONA speech synthesis.
Analyze the complete text in context and prepare it for natural machine narration.
Rules:
1. Preserve meaning, style, facts and language.
2. Keep natural punctuation and paragraphs; they control pauses.
3. Find words that a TTS engine may pronounce incorrectly. Pay special attention to:
- given names and surnames;
- place names;
- brands, products and company names;
- abbreviations;
- foreign words and loanwords;
- rare terms and homographs.
4. If only the stress is ambiguous, place U+0301 after the stressed vowel:
за́мок / замо́к, му́ка / мука́.
5. If a stress mark may be insufficient and you know the correct pronunciation with
high confidence, rewrite ONLY the problematic word phonetically:
- split it into readable parts with hyphens;
- write the stressed syllable in UPPERCASE;
- preserve the grammatical ending of the word form used in the text.
6. Do not limit the analysis to “Тарас”. Detect other risky words from context, but
do not invent a pronunciation when uncertain.
7. Format examples:
- Тарас → та-РАС
- OpenAI → оупен-ей-АЙ
- ChatGPT → чат-джі-пі-ТІ
- Renault → ре-НО
8. Do not syllabify ordinary words, insert IPA, or rewrite the entire script
phonetically. Preserve existing <emotion value="..."/> tags unchanged.
9. Return only the narration-ready text, without explanations, lists or comments.
My text:
"""
[PASTE TEXT]
"""Tags in text
The speed, volume and emotion fields set one style for the whole request. The same three controls can be placed inside the text instead — plus a pause, letter-by-letter spelling and a laugh. Tags work on sona-3.5 and sona-3.6; [laughter] is made for sona-3.6, the default model; do not rely on it with "model":"sona-3.5".
<speed ratio="0.8"/> | 0.6–1.5 | from here on |
<volume ratio="0.5"/> | 0.5–2.0 | from here on |
<emotion value="sad"/> | list below | from here on |
<break time="700ms"/> | ms or s, up to 10 s | one pause here |
<spell>Bob</spell> | — | read letter by letter |
[laughter] | — | one laugh here |
<volume ratio="0.6"/><speed ratio="0.8"/>This part is softer and slower.<break time="1s"/>
<volume ratio="1"/><speed ratio="1"/>And this part is back to normal.
<emotion value="excited"/> You will not believe what I learned! [laughter]
<emotion value="sad"/> But then everything went wrong…Uppercase, a comma decimal and a missing slash are repaired for you (<SPEED RATIO="0,8"> → <speed ratio="0.8"/>); broken markup returns 422 with an explanation instead of silently doing nothing. Text in angle brackets that is not a tag is voiced as a quotation: <<Stop!>> becomes «Stop!», 5 < 10 > 3 becomes 5 ‹ 10 › 3 (otherwise the engine silently skipped everything in brackets). HTML tags such as <b> are not spoken. Long text is split into parts and any active speed/volume/emotion is re-opened in each of them, so the effect does not stop mid-way.
emotion is in beta and works best in English — on other languages the difference may not be audible; speed and volume are guidance, not exact multipliers. <break> splits the generation at that point, so several in a row sound unnatural. Tag characters count towards chars but never reach subtitles: those contain only what is actually spoken.
Full list · 58 emotions
neutral, happy, excited, enthusiastic, elated, euphoric, triumphant, amazed, surprised, flirtatious, curious, content, peaceful, serene, calm, grateful, affectionate, trust, sympathetic, anticipation, mysterious, angry, mad, outraged, frustrated, agitated, threatened, disgusted, contempt, envious, sarcastic, ironic, sad, dejected, melancholic, disappointed, hurt, guilty, bored, tired, rejected, nostalgic, wistful, apologetic, hesitant, insecure, confused, resigned, anxious, panicked, alarmed, scared, proud, confident, distant, skeptical, contemplative, determined
Image generation
Generate from text or from image references.
POST /v1/images | JSON: prompt, model, size → image_id. Prefer: respond-async returns 202 immediately. |
POST /v1/images/edit | multipart repeated images: up to 10 files for Nano Banana 2.1 / Pro and GPT Image 2.5 |
GET /v1/images/{image_id} | Job status. On error: the reason in detail and error_code — content_refused (the model refused the request itself, refusal_reason as for video, retryable:false — change the prompt) or generation_failed (retryable:true); the charge is refunded automatically |
GET /v1/images/{image_id}/file | Download file |
GET /v1/images?limit=20 | Account history |
Example
# Nano plan or previously purchased credits: billing_source may be omitted (auto)
curl -X POST https://api.sonapro.ai/v1/images \
-H "X-API-Key: $SONA_API_KEY" -H "Content-Type: application/json" \
-H "Prefer: respond-async" \
-d '{"prompt":"A cinematic Crimean coast at sunrise","model":"nano-banana-2","size":"1024x1024"}'
# PAYG balance ($0.015): billing_source:"payg" is required; without it auto answers 402
curl -X POST https://api.sonapro.ai/v1/images \
-H "X-API-Key: $SONA_API_KEY" -H "Content-Type: application/json" \
-H "Prefer: respond-async" \
-d '{"prompt":"A cinematic Crimean coast at sunrise","model":"nano-banana-2","size":"1024x1024","billing_source":"payg","max_amount_microusd":15000}'
# edit with repeated references (repeat -F images=@... up to the model limit; for PAYG add -F billing_source=payg)
curl -X POST https://api.sonapro.ai/v1/images/edit \
-H "X-API-Key: $SONA_API_KEY" -H "Prefer: respond-async" \
-F 'prompt=Preserve the people and combine the composition' \
-F 'model=nano-banana-pro' \
-F 'images=@reference-1.jpg' -F 'images=@reference-2.png'
# GPT Image 2.5 through PAYG ($0.03); with previously purchased credits billing_source may be omitted
curl -X POST https://api.sonapro.ai/v1/images \
-H "X-API-Key: $SONA_API_KEY" \
-H "Content-Type: application/json" \
-H "Prefer: respond-async" \
-d '{"prompt":"Cinematic portrait in soft blue light","model":"gpt-image-2.5","size":"2048x1152","billing_source":"payg","max_amount_microusd":30000}'
# poll GET /v1/images/IMAGE_ID until status=done, then download download_urlSizes (size)
size | Format | Nano Banana 2.1 / Pro | GPT Image 2.5 |
|---|---|---|---|
1024x1024 | 1:1, square | 1024×1024 | ≈1254×1254 |
2048x1152 | 16:9, wide | 2048×1152 | ≈1672×941 |
1152x2048 | 9:16, vertical | 1152×2048 | ≈941×1672 |
1536x1152 | 4:3, landscape | 1536×1152 | ≈1448×1086 |
1152x1536 | 3:4, portrait | 1152×1536 | ≈1086×1448 |
The default is 1024x1024; any other value returns 422. Nano Banana returns a file of exactly the requested size. For GPT Image 2.5, size only picks the aspect ratio: the file arrives at the model's own resolution (~1.5 MP), never upscaled. POST /v1/images/edit takes the same size.
Models
| Model | model | References | Paid with |
|---|---|---|---|
| Nano Banana 2.1 | nano-banana-2 | up to 10 | Nano plan, PAYG or previously purchased credits |
| Nano Banana Pro | nano-banana-pro | up to 10 | the same as Nano Banana 2.1, sharing the plan's daily allowance |
| GPT Image 2.5 | gpt-image-2.5 | up to 10 | PAYG — $0.03, or previously purchased credits (at the purchase tariff) |
Do not hardcode the list: GET /v1/me returns in image.model_options only the models this account may use, with their price (billing_options). model is required; there is no default.
How an image is paid for
- Nano plan, 30 days: Start $27 — 600 images a day, 5 at once; Pro $45 — 1,200 a day, 7 at once; Ultra $90 — 3,500 a day, 14 at once. Nano Banana 2.1 and Pro count towards one daily allowance; unused allowance does not roll over.
- PAYG — $0.015 per generation from the money balance, only when asked for explicitly: send
billing_source:"payg"(abilling_source=paygfield in multipart). Addmax_amount_microusdto cap the price. - Previously purchased credits — 1,000 per Nano Banana 2.1 or Pro image.
- GPT Image 2.5 — PAYG, $0.03 per image (
billing_source:"payg"), or credits purchased before the switch to plans, at the purchase tariff (Start 2,500 · Pro 3,333 · Business 6,000 · Ultra 9,000);autospends those first. Plans and credits bought after the switch do not pay for it: without old creditsautoreturns402.sizeonly picks the aspect ratio — the file arrives at the model's own resolution (~1.5 MP, e.g. 1672×941 for 16:9). The legacy idsgpt-image-2andsona-imageare accepted.
The default billing_source:"auto" uses the plan, and without one the previously purchased credits. It never spends PAYG money: with neither a plan nor credits the answer is 402. A failed generation refunds automatically. Images ship as the 1080 original exactly as the model generates them, no upscale. quality=2k was retired and returns 422.
Video generation
Veo 3.1 · 720p · 8 s from text or references, currently 6 s from a first frame · one endpoint for text and images.
POST /v1/videos | Create a clip → 202 + video_id. JSON for a clip from text; multipart/form-data to start from 1–3 images |
GET /v1/videos/{video_id} | Status: queued → processing → done or error |
GET /v1/videos/{video_id}/file | Download the MP4 (H.264 + AAC) |
GET /v1/videos?limit=20 | Account clip history |
Parameters
| Field | Default | Description |
|---|---|---|
prompt * | — | What happens in the shot, up to 4000 characters |
model | veo-3.1 | The one video model |
aspect_ratio | 16:9 | 16:9 (landscape) or 9:16 (portrait) |
images | — | Multipart only, repeated field, PNG/JPEG/WEBP up to 10 MB each. 1 image is the first frame; 2 are the first and last frames; 3 are references (a character, an object, a style). More than three returns 422 |
image_mode | by count | The images' role instead of the count rule: frames — 1–2 images as the first / first and last frame; references — 1–3 images as references. frames with three images returns 422 |
resolution | 720p | 720p or 1080p. 1080p only with billing_source:"payg", $0.03; when 1080p is unavailable right now it returns 503 and nothing is charged. The status reports the finished file's resolution: if the upscale to 1080p did not succeed, the clip comes at 720p and $0.02 is charged |
billing_source | auto | auto, plan or payg, as for images |
max_amount_microusd | — | PAYG price ceiling; anything dearer is refused with 409 and nothing is charged |
Always asynchronous: a clip takes one to several minutes to render, so POST answers 202 at once and you fetch the file from the status's download_url. A clip from text is 8 s. The render service picks the length of a clip from images: currently 6 s from a first frame (1–2 images) and 8 s from three references. So its duration_s is null until it is done. A finished clip's duration_s is the real length of the file.
Example
# 1. A clip from text
curl -X POST https://api.sonapro.ai/v1/videos \
-H "X-API-Key: $SONA_API_KEY" -H "Content-Type: application/json" \
-d '{"prompt":"A paper boat drifting across a calm pond at sunrise","aspect_ratio":"16:9"}'
# → 202 {"video_id":"...","status":"queued","model":"veo-3.1","status_url":"/v1/videos/..."}
# 2. A clip that starts from your picture
curl -X POST https://api.sonapro.ai/v1/videos \
-H "X-API-Key: $SONA_API_KEY" \
-F 'prompt=The camera slowly pushes in, wind moves the leaves' \
-F 'aspect_ratio=9:16' \
-F 'images=@first-frame.jpg'
# 3. Poll until done, then download the MP4
curl https://api.sonapro.ai/v1/videos/VIDEO_ID -H "X-API-Key: $SONA_API_KEY"
curl https://api.sonapro.ai/v1/videos/VIDEO_ID/file -H "X-API-Key: $SONA_API_KEY" -o clip.mp4Access and billing
- Video is available to every account. If a manager has switched Veo off for an account,
POST /v1/videosreturns403andGET /v1/meshowsvideo.allowed:false. - Veo plan, 30 days, unlimited clips: Start $45 — 2 clips at once; Pro $95 — 6; Ultra $140 — 10. A manager enables the plan.
- PAYG — $0.02 per clip from the money balance, only when asked for explicitly: send
billing_source:"payg". To lock the price, addmax_amount_microusd(20000). A 1080p clip (resolution:"1080p") is $0.03 (30000), PAYG only.
GET /v1/me → video: allowed, models, aspect_ratios, duration_s, max_references, resolutions (available now) and billing_options, this account's price. If a render fails, the status says error with the reason in detail, and the payment is returned automatically. error_code is content_refused when the model refused the request itself under its policies (refusal_reason: real_person, intellectual_property, minor, audio, sexual, violence, unsafe, prohibited, prompt or policy; retryable:false — change the prompt or the image), otherwise generation_failed (retryable:true). The same request (prompt + images) refused twice (three times when the reason is the audio) is not accepted for 24 h: POST answers 422 at once and nothing is charged.
Results & errors
Poll the status endpoint until a terminal state.
Job lifecycle
queued→processing→donequeued→processing→errorPoll every 1–2 seconds. Download audio, subtitles, images or video only after done.
Job history — GET /v1/activity
The latest jobs of the account that owns the key: voice-overs, images and video together. Unfinished ones (queued, processing) come first, then newest first. It shows which requests have finished and which have not, even if a job_id was lost.
kind | voice, image or video; several, comma-separated. Omit for all. |
status | queued, processing, done, error or canceled; several, comma-separated. Omit for all. |
limit | How many jobs to return: 1 to 100, 12 by default. |
curl "https://api.sonapro.ai/v1/activity?kind=voice&status=queued,processing,error&limit=50" -H "X-API-Key: $SONA_API_KEY"Response, shortened:
{
"jobs": [
{"id": "3f9c2a…", "kind": "voice", "name": "chapter_2", "status": "processing",
"created_at": "2026-10-09T10:27:03+00:00", "finished_at": null},
{"id": "8a41d0…", "kind": "voice", "name": "chapter_1", "status": "done",
"created_at": "2026-10-09T10:12:40+00:00", "finished_at": "2026-10-09T10:13:05+00:00"}
],
"payments": []
}id is the voice-over's job_id, the image's image_id or the clip's video_id: fetch status and files with the usual requests (/v1/tts/{job_id}/audio, /v1/images/{image_id}/file, /v1/videos/{video_id}/file). Files are kept for a limited time; the history entry stays. An unknown kind or status value returns 422. payments lists the account's latest payments.
HTTP codes
400 / 422 | Invalid input, unsupported voice, language or format |
401 | Missing, invalid or revoked API key |
402 | Insufficient credits, no active plan, or an empty PAYG balance |
403 | The model or product is not enabled for this account (e.g. video before a manager enables it) |
404 | Job, template or file not found |
409 | The price is above the ceiling you sent (max_credits / max_amount_microusd); nothing was charged |
429 | Rate limit, every concurrent job slot busy, or the plan's daily allowance spent; retry after a delay |
503 | Engine is temporarily paused; show the returned maintenance message |