SONA Voices Telegram bot UA
SONA reference

Settings

Everything required for the bot and API: authentication, voices, pronunciation, subtitles, images, files and errors.

Key & quick start

The bot and API share the same credit balance.

Get an API key

  1. Open @sonapro_bot.
  2. Choose 🔌 API → Create key.
  3. 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.mp3

Endpoints

Base URL: https://api.sonapro.ai.

MethodPathPurpose
POST/v1/ttsCreate speech or a dialogue → job_id
GET/v1/tts/{job_id}Job status
GET/v1/tts/{job_id}/audioDownload audio
GET/v1/tts/{job_id}/linkShort public link
GET/v1/tts/{job_id}/subtitles?format=srt|vtt|ass|json|zipSubtitles; requires subtitles:true
GET/v1/tts/{job_id}/dubDubbing: languages, voices and price (credits and PAYG) for a finished result
POST/v1/tts/{job_id}/dubDub a finished result → one job per language; paid in credits or PAYG
POST/v1/tts/dub-optionsDubbing languages and voices before a job exists
GET/v1/tts/{job_id}/fragmentsFragments of a finished voice-over: number, text, start and end
POST/v1/tts/{job_id}/fragments/{n}/quotePrice of voicing a fragment again
POST/v1/tts/{job_id}/fragments/{n}Voice a fragment again → a new whole file
POST/v1/tts/dialogue/parseDialogue: split a script or prose into roles and lines
GET/v1/tts/{job_id}/dialogueDialogue: who says what, and when
GET/v1/tts/{job_id}/stemsDialogue: a ZIP with a separate track per role
GET/v1/tts/{job_id}/stems/linkPublic link to the role tracks
GET/POST/v1/dialogue/templatesDialogue 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_v1Catalog; collection: library or sona_v1
POST/v1/voices/cloneClone a voice → tmpl:N
GET/POST/v1/voices/templatesList or create a template
PATCH/DEL/v1/voices/templates/{num}Edit or delete a template
GET/v1/emotionsCurrent emotion list
GET/POST/v1/pronunciationList or save a pronunciation rule
DEL/v1/pronunciation/{term}Delete a rule
GET/v1/meAccount, tariff and balance
GET/v1/activity?kind=voice&status=done&limit=50Account job history: what has finished and what has not (voice, images, video)
POST/v1/imagesGenerate an image
POST/v1/images/editGenerate with image references
GET/v1/images/{image_id}Job status
GET/v1/images/{image_id}/fileDownload PNG/JPEG/WebP
POST/v1/videosCreate 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}/fileDownload MP4
GET/v1/videos?limit=20Account 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.

FieldVoice-over credits and tariff
credits_remainingWhat can be spent now: every live pack plus the starter credits. Next to it: credits_total and credits_used.
credits_expiring
credits_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.
tariffCurrent tariff: start, pro, business, ultra or null.
tariff_expires_atWhen 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_limitHow many voice-overs the account may run at once.
sub_active
sub_expires_at
The Ultra subscription and its end; false and null on other tariffs.
unlimitedtrue — 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.products.nano.plan and product_billing.products.video.plan; null without a plan.

FieldNano / Veo plan
daily_limit
jobs_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_today
remaining_today
Used and left in the current day.
resets_atWhen the next day starts; with several plans, the soonest one. Days count from the start of the plan, not from midnight.
expires_atEnd of the plan's current term; with several plans, the soonest.
active_untilEnd of the whole unbroken paid time. Without other plans it equals expires_at.
plansEach 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.legacy_image_credits — 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 / character
sona_…SONA Voice v11.5 credits / character
tmpl:NPersonal template/clone1.5 credits / character

Build 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.

FieldType / rangeMeaning
text *stringText to synthesize
voice *vc_…, sona_…, tmpl:NVoice or template
languageauto, uk, en…auto uses the voice language
modelsona-3.5 / sona-3.6Defaults 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.
normalizationauto / off / localeSona 3.6 spoken numbers, dates, times, currencies and abbreviations; e.g. en-IN
formatmp3Output format
bitrate32000–192000MP3 bitrate; default 96000
sample_rate8000–48000Sample rate; default 44100
speed0.6–1.5SONA/clone speed
volume0.5–2.0SONA/clone volume
emotionstringOne delivery style for the request
billing_sourcecredits / paygDefault: plan credits. Set payg explicitly for your USD wallet. No automatic fallback.
auto_stressbool, default trueInternal Cyrillic capital vowel → U+0301
subtitlesboolSRT/VTT/ASS and JSON timings
namestringFilename without extension
client_request_idstring, 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).

RequestWhen
GET /v1/tts/{job_id}/dubThe offer for a finished result: languages, voices and price
POST /v1/tts/{job_id}/dubDub a finished result → one job per language
POST /v1/tts + dub_languagesOrder dubs together with the voice-over: the jobs are created for you once the original is done
POST /v1/tts/dub-optionsLanguages and voices before the voice-over exists
POST /v1/audio/dub-sourceDub 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.

ModeHow it soundsWhen 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.mp3

In 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_id

Once 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.

ValueWhat you hear
autoThe 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.
sameThe original voice, accent included
nativeThe most popular native-speaker catalog voice, of the same gender when it is known
vc_…, sona_…, tmpl:NA 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 *_max fields); 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, and sona-3.6 adds 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 is 429. free_slots shows 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.

402Not enough credits (the message suggests paying with PAYG) or not enough PAYG funds
403Dubbing is not available for this account, or PAYG is disabled for the account
404Job not found
409The 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
410The result has already been deleted after its storage period
422Language unavailable or the same as the original's, too many languages, unknown voice, mode other than video or natural
429Not enough free slots for every language
503Dubbing 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

  1. Open @sonapro_bot and send an audio file or a voice message — up to 20 MB (Telegram gives bots nothing larger).
  2. The bot shows the recording's card with its length and price — press «🌐 Dub this audio».
  3. Wait for the recognition — about a minute for every 10 minutes of recording. The bot sends the recognised text.
  4. 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).
  5. «🌐 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 ⭐.
  6. 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 usual POST …/dub.
RequestWhat it does
POST /v1/audio/dub-sourceMultipart: 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}/transcriptThe recognised text and its sentences with their times (sentences)
PUT /v1/tts/{job_id}/transcriptCorrect the text before dubbing: unchanged words keep their time in the recording
GET / POST /v1/tts/{job_id}/dubThe 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.
411No Content-Length header — send the file as an ordinary request
413The file is larger than 95 MB
422The 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
429The 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.

RequestWhat it does
POST /v1/tts + speakers, segmentsVoice a dialogue → job_id; POST /v1/tts/quote with the same body shows the price first
POST /v1/tts/dialogue/parseSplit a script or plain prose into roles and lines — free
GET /v1/tts/{job_id}/dialogueA finished dialogue: who says what, and when
GET /v1/tts/{job_id}/stemsA ZIP with a separate track per role
GET /v1/dialogue/templatesDialogue templates dlg:N: heroes and their voices
POST /v1/tts/{job_id}/dub + role_voicesDub 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

  1. 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».
  2. 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.
  3. 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 tracksA ZIP with a separate track per role; a link to it when it is over 50 MB
✂️ Voice a fragment againThe lines as a list «Name: text»: any of them can be voiced again — as it was or with corrected text
🌐 Create dubbingThe dialogue in other languages: you choose only the languages, every role's voice for every language is picked for you
💾 Save as a templateThe 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: text or [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 ms or мс — milliseconds.
CommandUkrainian · RussianWhat it does
{pause: 1.5}{пауза: 1.5} in bothA 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."}]}'
FieldDescription
speakers2–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_secondsThe pause between lines that set none of their own, 0–30 s; 0.35 by default
templatedlg:N — a dialogue template: every role gets the voice of the hero with the same name
textNot 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
nameThe name in subtitles, tracks and templates; defaults to id
voice *vc_…, sona_… or tmpl:N
speed0.6–1.5 (or 60–150 %)
volume0.5–2.0 (or 50–200 %): an exact gain for the role in the mix, after the loudness is levelled
emotionThe role's emotion
languageThe 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_secondsThe pause after the line, 0–30 s; replaces dialogue_pause_seconds
start_modeWhen the line starts — table below; after by default
start_offset_secondsFor overlap and after_seconds, 0–60 s
start_after_wordsFor after_words: a phrase of up to 200 characters that occurs exactly once in the previous line
interrupt_previoustrue — cut the previous line where this one starts (with any start_mode but after)
interrupt_overlap_secondsHow long both voices sound together before the cut, 0–0.5 s; 0.05 by default
start_modeWhen the line starts
afterThe default: once every previous line has ended, plus the pause
simultaneousTogether with the previous line
overlapstart_offset_seconds before the previous line ends
after_secondsstart_offset_seconds after the previous line starts
after_wordsAs 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 …/stems returns a ZIP: a separate track per role, as long as the whole dialogue, plus timeline.json. GET …/stems/link is a public link that needs no key (url and expires), 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}/fragments returns the lines with speaker_id and speaker_name. POST …/fragments/{n} with an optional corrected text creates a new job — the whole dialogue with that line voiced again; the original does not change. POST …/fragments/{n}/quote shows 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's voice (the first role only) → a catalog voice no other role has. voice_source says where it came from: template, memory, current, suggested or null. A template or memory voice comes with its speed and emotion; those voices themselves are in template_voice and remembered_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 has template — id, name, matched and missing (roles the template gave no voice), otherwise null.
  • segments can go straight into POST /v1/tts once every role has a voice. Their fields use the short names — pause_after, start_offset, interrupt, interrupt_overlap; POST /v1/tts accepts both spellings.
  • A script that could not be voiced (a broken command, too many roles or lines, too long) gets 422 with 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.

RequestWhat it does
GET /v1/dialogue/templatesThe list: templates, the most recently changed first, and max
POST /v1/dialogue/templatesCreate → 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 gets 422 «Рядок N: …» (line N).
  • Matching by name ignores case and extra spaces, «ё» = «е», and every apostrophe is the same. POST /v1/tts with "template": "dlg:3" gives every role the voice of the hero with the same name, while subtitles keep the script's spelling; speakers with the same id override a hero or add a new one. A role found in neither gets 422 naming 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:N is deleted, the heroes that used it are left without a voice ("voice": null): the parse lists them in missing, 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_… or tmpl:N; a role without an entry gets auto. voices does 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 …/dub of a dialogue adds dialogue: true, roles and role_voices — for every language and role, what auto picks and up to 6 native voices in choices.
  • 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_credits work 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 id and 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_seconds and the numbers in commands) — up to 60 s. The whole dialogue including pauses — up to 5 hours; a longer one gets 422 before 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.

400The request body could not be read — for example, it is not UTF-8
402Not enough credits or PAYG funds
403Dialogues may not be enabled for this account yet (GET /v1/me → "dialogue": false), or PAYG is disabled for the account
404Job or template not found, the job is not a dialogue, or there is no line with that number
409The dialogue is not finished yet, the price is above max_credits / max_microusd, 50 templates are already saved, or it is already a dub
410The dialogue's files have already been deleted after their storage period
422A 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
429Too many requests (parsing — 30 a minute, with auto — 6) or every concurrent job slot is busy
503Dialogues 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

LevelExampleWhen 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.

PropertySONA behavior
CyrillicSupported in both term and replacement. Sounds-like guidance may use ordinary Ukrainian or Russian letters.
CaseBy default, Тарас matches Тарас, тарас and ТАРАС.
BoundariesOnly a complete word or complete phrase matches. Тарас does not modify Тараса or Тарасові.
InflectionsSave each required word form separately: Тараса → та-РА-са, Тарасові → та-РА-со-ві. These demonstrate the format; listen-test every replacement.
ScopeRules belong to the account, not one voice. They apply in the bot and API to all of its voices and templates.
OrderLonger 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.5from here on
<volume ratio="0.5"/>0.5–2.0from here on
<emotion value="sad"/>list belowfrom here on
<break time="700ms"/>ms or s, up to 10 sone 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/imagesJSON: prompt, model, size → image_id. Prefer: respond-async returns 202 immediately.
POST /v1/images/editmultipart 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}/fileDownload file
GET /v1/images?limit=20Account 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_url

Sizes (size)

sizeFormatNano Banana 2.1 / ProGPT Image 2.5
1024x10241:1, square1024×1024≈1254×1254
2048x115216:9, wide2048×1152≈1672×941
1152x20489:16, vertical1152×2048≈941×1672
1536x11524:3, landscape1536×1152≈1448×1086
1152x15363:4, portrait1152×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

ModelmodelReferencesPaid with
Nano Banana 2.1nano-banana-2up to 10Nano plan, PAYG or previously purchased credits
Nano Banana Pronano-banana-proup to 10the same as Nano Banana 2.1, sharing the plan's daily allowance
GPT Image 2.5gpt-image-2.5up to 10PAYG — $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" (a billing_source=payg field in multipart). Add max_amount_microusd to 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); auto spends those first. Plans and credits bought after the switch do not pay for it: without old credits auto returns 402. size only 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 ids gpt-image-2 and sona-image are 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/videosCreate 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}/fileDownload the MP4 (H.264 + AAC)
GET /v1/videos?limit=20Account clip history

Parameters

FieldDefaultDescription
prompt *—What happens in the shot, up to 4000 characters
modelveo-3.1The one video model
aspect_ratio16:916: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_modeby countThe 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
resolution720p720p 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_sourceautoauto, 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.mp4

Access and billing

  • Video is available to every account. If a manager has switched Veo off for an account, POST /v1/videos returns 403 and GET /v1/me shows video.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, add max_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→done
queued→processing→error

Poll 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.

kindvoice, image or video; several, comma-separated. Omit for all.
statusqueued, processing, done, error or canceled; several, comma-separated. Omit for all.
limitHow 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 / 422Invalid input, unsupported voice, language or format
401Missing, invalid or revoked API key
402Insufficient credits, no active plan, or an empty PAYG balance
403The model or product is not enabled for this account (e.g. video before a manager enables it)
404Job, template or file not found
409The price is above the ceiling you sent (max_credits / max_amount_microusd); nothing was charged
429Rate limit, every concurrent job slot busy, or the plan's daily allowance spent; retry after a delay
503Engine is temporarily paused; show the returned maintenance message