Audio API (TTS & STT)
OpenAI-compatible audio API – text-to-speech with German voices and voice cloning, transcription via Voxtral, processed in EU data centres.
The audio API offers speech synthesis (POST /v1/audio/speech) and transcription
(POST /v1/audio/transcriptions) in the OpenAI format. You can keep using existing OpenAI
audio clients: point baseURL at us and pick one of our voices. Speech is generated and
recognised in EU data centres.
Both endpoints require an API key with the matching scope: speech for TTS, transcribe
for STT (see Authentication → Permissions).
There are four engines for speech synthesis. You pick one per request through the
OpenAI-standard model field (supertonic-3 / cosyvoice-3 / voxtral-mini-tts /
qwen3-tts) or through provider. If you set neither, the server derives the engine from
the voice, otherwise the default engine applies: Mistral since 26 September 2026 (EU
endpoint, France), Supertonic before that. To keep using Supertonic, set
provider: "supertonic" or model: "supertonic-3", or pick a Supertonic voice
(M1–M5/F1–F5). If Mistral does not speak the language (from language or, with
auto, detected from the text), Qwen takes over (e.g. ru, ja, ko, zh), otherwise
Supertonic (e.g. tr, uk, pl, ro). X-Voice-Provider tells you which engine spoke.
provider | Strength | Voices | Languages | Emotion / tags | Cloning | Format |
|---|---|---|---|---|---|---|
supertonic | fast, no cold start | M1–M5 / F1–F5 | 31, chosen via language | <breath>/<sigh> | – | wav/flac |
cosyvoice | German with emotion and cloning | de-thorsten or your own voice | 9, follows the text | full inline tags plus natural-language emotion | yes (zero-shot) | wav (24 kHz) |
mistral (default) | Default engine and voice catalogue: designed German voices | bib-m-40, bib-f-35, … (see Voices) | 9, follows the text | – | – | mp3/wav/opus/flac |
qwen | our own voices without Mistral, preliminary | rooom-f-01, rooom-f-02, rooom-m-01, rooom-m-02 | 10, chosen via language | – | – | wav (24 kHz) |
The language counts are those of each model card. We have checked (round-trip
transcription) mainly German, plus English for the catalogue voices and Russian for qwen.
There is no measurement of our own for the other languages.
Transcription (STT) runs on Mistral Voxtral through the EU endpoint of Mistral AI (France).
Alternatively, Whisper large-v3 transcribes with 99 languages (model=whisper-large-v3, see
below); that model is self-operated on rented GPUs in EU data centres (and additionally in
Iceland or Norway, EEA).
AI disclosure: synthetically generated speech must be labelled as AI content (EU AI Act Art. 50). WAV, FLAC, MP3 and Opus output carries metadata for this, CosyVoice clips also an inaudible watermark (details: AI labelling). Telling your listeners is up to your application. Supertonic weights are under OpenRAIL-M.
POST /v1/audio/speech — text-to-speech
Produces spoken audio from text. The response is the raw audio body with no JSON wrapper, just like OpenAI's.
curl https://sovrgpt.com/api/v1/audio/speech \
-H "Authorization: Bearer $SOVR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "supertonic-3",
"input": "Guten Tag <breath> willkommen bei SovrGPT.",
"voice": "F3",
"response_format": "wav"
}' --output hello.wavRequest body
| Field | Type | Applies to | Description |
|---|---|---|---|
input | string | all | Required. The text to speak, 4,000 characters or fewer (2,000 for qwen). Inline tags depend on the provider (see below). |
voice | string | all | Catalogue voice by slug (bib-m-40, bib-f-35, …; list: GET /v1/audio/voices, samples: Voices); a catalogue voice pins provider: mistral. Also Supertonic: M1–M5/F1–F5. CosyVoice: de-thorsten. Qwen: rooom-f-01, rooom-f-02, rooom-m-01, rooom-m-02 (pins provider: qwen). These voices belong to their engine: with a different provider or model you get 400 voice_provider_mismatch, also with a reference clip. default-de/default-en is the system default: spoken by the default engine (currently a Mistral voice), with provider: supertonic by Supertonic F1 or M1. |
provider | string | all | supertonic | cosyvoice | mistral | qwen. Optional; otherwise derived from model/voice, otherwise the server default (currently mistral). |
response_format | string | Supertonic/Mistral | wav/flac (Supertonic; any other value yields wav), plus mp3/opus/aac (Mistral). CosyVoice and Qwen always return wav. |
language | string | Supertonic/Qwen | The spoken language as ISO 639-1 (de, en, fr, …; de-DE counts as de) or auto (detect from the text). Default: de (en for default-en). Supertonic knows 31 codes (en ko ja ar bg cs da de el es et fi fr hi hr hu id it lt lv nl pl pt ro ru sk sl sv tr uk vi); any other code yields 400 language_not_supported. Qwen knows 10 (de en fr es it pt ru ja ko zh); any other yields 422 language_not_supported. The error response lists the valid codes in supported. Mistral and CosyVoice have no language parameter: the language follows the text and the field is ignored. |
emotion / instruction | string | CosyVoice | Natural-language style or emotion instruction, e.g. "Sprich sehr traurig und langsam." |
reference_audio_b64 | string | CosyVoice | Base64 wav/flac (30 s or less): clones that voice (zero-shot). Overrides voice. With provider: qwen you get 400 cloning_not_supported. |
prompt_text | string | CosyVoice | Transcript of the reference clip (best quality). Automatic for built-in voices. |
speed | number | CosyVoice | 0.5–2.0 (default 1.0). Supertonic/Mistral/Qwen: accepted, currently ignored. |
model | string | all | Selects the engine (the OpenAI way): supertonic-3, cosyvoice-3, voxtral-mini-tts or qwen3-tts. If both model and provider are set, provider wins. Any other value, including tts-1 or gpt-4o-mini-tts, is not rejected: voice or the default decides. x-sovrgpt-model-served tells you which model spoke. |
Response
200 with audio bytes. Headers:
Content-Type: audio/wav (or audio/flac, audio/mpeg …)
X-Voice-Provider: supertonic (or: cosyvoice / mistral / qwen)
X-Voice: bib-m-40 (catalogue slug that spoke — or: default)
X-Voice-Language: en (Supertonic/Qwen only: the language that was spoken)
x-sovrgpt-model-served: voxtral-mini-tts (the model that spoke)
x-sovrgpt-model-requested: tts-1 (only if `model` differs from it)Unlike chat, embeddings and rerank, the audio endpoints will not start rejecting an unknown
model value. Check x-sovrgpt-model-served if you expect a particular engine.
| Status | Meaning |
|---|---|
| 400 | input missing or too long (text_too_long for qwen above 2,000 characters); unknown_voice: the slug does not exist, is disabled or does not belong to qwen; voice_provider_mismatch: the voice belongs to a different engine than provider/model, e.g. rooom-f-01 with model: "supertonic-3"; language_not_supported (Supertonic); cloning_not_supported (qwen with a reference clip). |
| 401 / 403 | key or scope missing. 403 voice_not_in_plan: the organisation does not have voice selection (from Starter or via the “Voices” entitlement, see access in GET /v1/audio/voices). |
| 422 | language_not_supported (qwen): the language is not one of the ten Qwen languages, see supported. |
| 429 | busy (qwen): all speech streams are taken. Resend after one second (Retry-After: 1). A 429 from Mistral is passed on without Retry-After, because Mistral names no waiting time. |
| 502 | upstream error; for qwen also a speech stream that broke off early (stream_incomplete). We never deliver half an announcement. |
| 503 | TTS provider not configured, or the CosyVoice or Qwen worker is cold-starting; resend in about a minute |
Errors come as {"error": {"message", "type", "code"}}; for language_not_supported the
error object also carries the list supported. When the speech service itself fails
(upstream_error, service not configured), message is a fixed sentence. Branch on type
and code, not on the wording. For a support request, the time of the request is enough.
GET /v1/audio/voices — the voice catalogue
Which values voice accepts, and whether this organisation may select them. Answers for any
valid key, no scope required, like GET /v1/models.
curl https://sovrgpt.com/api/v1/audio/voices -H "Authorization: Bearer $SOVR_KEY"{
"object": "list",
"data": [
{
"object": "voice",
"slug": "bib-m-40",
"label": "Männlich, Anfang 40",
"origin": "designed",
"gender": "m",
"age_group": "Anfang 40",
"description": "Warm und rund, weicher Stimmansatz, ruhige Satzmelodie.",
"primary_language": "de",
"languages": ["de", "en", "fr", "es", "it", "pt", "nl", "hi", "ar"],
"samples": { "de": "/api/voice/samples/bib-m-40?lang=de", "en": "/api/voice/samples/bib-m-40?lang=en" },
"scope": "platform",
"sample_url": null,
"provider": "mistral",
"model": "voxtral-mini-tts",
"selectable": true,
"available": true
},
{
"object": "voice",
"slug": "rooom-f-01",
"label": "Beraterin (vorläufig)",
"origin": "designed",
"gender": "f",
"age_group": "30–35",
"primary_language": "de",
"languages": ["de", "en", "fr", "es", "it", "pt", "ru", "ja", "ko", "zh"],
"provider": "qwen",
"model": "qwen3-tts",
"selectable": true,
"available": true,
"status": "preliminary",
"status_note": "vorläufig — die Hörfreigabe steht aus; Klang und Kennung können sich noch ändern."
}
],
"languages": ["de", "en", "fr", "es", "it", "pt", "nl", "hi", "ar"],
"languages_by_provider": {
"mistral": ["de", "en", "fr", "es", "it", "pt", "nl", "hi", "ar"],
"supertonic": ["en", "ko", "ja", "ar", "bg", "cs", "da", "de", "el", "es", "…"],
"cosyvoice": ["zh", "en", "ja", "ko", "de", "es", "fr", "it", "ru"],
"qwen": ["de", "en", "fr", "es", "it", "pt", "ru", "ja", "ko", "zh"]
},
"access": { "selectable": true, "via": "tier", "tier": "pro", "hint": null }
}(Shortened: data contains every catalogue voice plus the fixed voices of the engines,
i.e. M1–M5/F1–F5 from Supertonic, de-thorsten from CosyVoice and the four
rooom-* voices from Qwen.)
provider/model: which engine speaks the voice. A catalogue voice pinsmistral, arooom-*voice pinsqwen.selectable: whether this organisation may pick the voice. Catalogue voices depend on the plan (seeaccess); the fixed engine voices do not.available: whether the engine is set up in this environment. It says nothing about whether a worker is running right now (cold start: see below).status: "preliminary": the voice is preliminary. The fourrooom-*voices are still waiting for listening approval; until then their sound and identifier may change. For production use that needs a fixed voice, that is a reason to wait.languagesstays the list of the Mistral voices for compatibility;languages_by_providerlists the languages per engine.origin:designed= generated from a written description, fictional (belongs to no person);preset= the provider's stock voice;cloned= from a recording.languagesper voice: the languages of its engine according to the model card. For the catalogue voices these are the nine Mistral languages. The designed voices were designed for German. We checked German and English for the catalogue voices (round-trip transcription word for word, no onset click); in the other languages the provider states that a colouring of the source language remains audible.access.selectable:falsemeansPOST /v1/audio/speechanswers403 voice_not_in_planfor catalogue voices.vianames the reason for access (tierfrom Starter,entitlementvia the “Voices” entitlement).samplesare public and play without a key (fixed sample sentence, MP3).scope:platform= available to every organisation with voice selection;org= a voice you designed yourself, visible to your organisation only. Org voices carry no publicsamples, because the sample route is reachable without signing in. Instead they carry a signedsample_urlvalid for one hour.
Example with a catalogue voice (provider is then redundant):
curl -X POST https://sovrgpt.com/api/v1/audio/speech -H "Authorization: Bearer $SOVR_KEY" -H "Content-Type: application/json" -d '{"input":"Hello, this is a designed voice speaking.","voice":"bib-f-35","response_format":"mp3"}' -o hello.mp3Design your own voice
Have a fictional voice generated from a description, stored as a reference clip and adopted
into the catalogue. Every design endpoint needs the speech scope and a key whose creator
is an owner or admin of the organisation. The exception is the options endpoint: like
GET /v1/models, it only needs a valid key, and access.designable tells you whether this
key may design.
| Method | Path | What it does |
|---|---|---|
GET | /v1/audio/voices/design-options | The casting sheet: traits, limits, whether it is switched on |
POST | /v1/audio/voices/designs | Order a design (202, keeps running) |
GET | /v1/audio/voices/designs | This organisation's designs |
GET | /v1/audio/voices/designs/{id} | Fetch the state; drives the job forward |
POST | /v1/audio/voices/designs/{id}/adopt | Clone and register in the catalogue |
DELETE | /v1/audio/voices/designs/{id} | Discard, delete the reference clip |
POST | /v1/audio/voices/designs/suggest | Propose traits from a free description |
GET /v1/audio/voices/design-options reports in available whether the design service is
switched on in this environment. If it says false, POST …/designs answers
501 not_implemented.
Ordering
curl -X POST https://sovrgpt.com/api/v1/audio/voices/designs \
-H "Authorization: Bearer $SOVR_KEY" -H "Content-Type: application/json" \
-d '{
"language": "en",
"gender": "f",
"age_years": 52,
"timbre": "warm",
"character": "calm",
"tempo": "even",
"use_case": "narration",
"free_text": "slightly smoky, fundamental around 200 hertz",
"label": "Narrator, calm",
"slug": "narrator-calm",
"auto_adopt": false
}'{
"object": "voice_design",
"id": "9f1c…",
"status": "pending",
"scope": "org",
"target_hz": 203,
"band_hz": [185, 255],
"auto_adopt": false,
"measurements": { "f0_hz": null, "in_band": null, "attempts_used": null, "seconds": null },
"sample_url": null,
"cost_micro_eur": 0,
"voice_slug": null,
"error": null
}gender is required, plus one of age_years (16–90) or age_group. Everything else is
optional. Whatever you leave out is drawn at random per job, so two identical orders can
yield different voices. The valid trait ids come from design-options; an unknown id
answers 400 invalid_request_error and names the allowed ones.
Professional fields: instruct replaces the generated description entirely (max. 2000
characters), target_hz pins the register (60–400).
Fetching
curl https://sovrgpt.com/api/v1/audio/voices/designs/9f1c… \
-H "Authorization: Bearer $SOVR_KEY"The call also drives the job: it asks the worker, downloads a finished clip and completes
the job. Poll it every few seconds until status is no longer pending. A job that is not
finished after 20 minutes is set to failed.
{
"object": "voice_design",
"id": "9f1c…",
"status": "ready",
"measurements": {
"f0_hz": 208.4, "p25_hz": 196, "p75_hz": 221,
"voiced_windows": 340, "in_band": true, "attempts_used": 2, "seconds": 19.5
},
"sample_url": "https://…signed… (valid for 1 h)",
"duration_ms": 42000,
"cost_micro_eur": 7583,
"voice": null
}in_band: false means none of the draws hit the ordered register and the best one was kept.
That is not an error, but a good reason to listen before adopting.
States: pending → ready → adopted, plus discarded and failed.
Adopting
curl -X POST https://sovrgpt.com/api/v1/audio/voices/designs/9f1c…/adopt \
-H "Authorization: Bearer $SOVR_KEY" -H "Content-Type: application/json" \
-d '{"slug":"narrator-calm","label":"Narrator, calm"}'Answers with the finished catalogue entry (scope: "org"). The voice is immediately
available in GET /v1/audio/voices and as voice in POST /v1/audio/speech.
To do it in one go, set auto_adopt: true when ordering: the job then moves from ready
straight to adopted without anyone having to hear the sample. That is the path for
unattended integrations; the app deliberately does not offer it.
Proposal from a description
curl -X POST https://sovrgpt.com/api/v1/audio/voices/designs/suggest \
-H "Authorization: Bearer $SOVR_KEY" -H "Content-Type: application/json" \
-d '{"text":"calm, a bit older, like a narrator in an audiobook"}'Returns trait ids, age_years, gender, a display name, a rationale and up to three
hints. It is a proposal you can pass straight into POST …/designs or adjust first. The
call designs nothing and starts no worker.
No real people. A named public figure is translated into audible traits and not adopted. Describe what can be heard, nothing about appearance or origin.
Limits and error codes
Per organisation 10 adopted voices and 3 designs running at once (the current values are in
design-options.limits).
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_request_error | unknown trait id, age outside 16–90, invalid slug |
| 402 | quota_exceeded | spend limit of the organisation or user reached |
| 403 | voice_not_in_plan | plan below Starter and no “Voices” entitlement |
| 403 | voice_role_required | the key's creator is not an owner or admin of the organisation |
| 409 | voice_limit_reached | maximum own voices reached (or slug taken) |
| 409 | too_many_pending | too many designs running at once |
| 501 | not_implemented | the design service is not switched on in this environment |
| 502 | upstream_error | worker or runtime unreachable |
Billed is the pure synthesis time of the design (duration_ms), not the cold start. It
shows as cost_micro_eur on the job and as a usage line (voice-design:qwen3-tts).
CosyVoice: German with emotion, tags and your own voice
For expressive German with emphasis, laughter, breathing and voice cloning, pick the
cosyvoice provider. Full capabilities: Models → Voice.
Inline tags (directly inside input): [laughter], [breath],
<laughter>…</laughter> (spoken while laughing), <strong>…</strong> (emphasis).
# German with tags and emphasis
curl https://sovrgpt.com/api/v1/audio/speech \
-H "Authorization: Bearer $SOVR_KEY" -H "Content-Type: application/json" \
-d '{
"provider": "cosyvoice",
"voice": "de-thorsten",
"input": "Und dann [breath] öffnete ich die Tür. [laughter] Das war <strong>unglaublich</strong>."
}' --output tags.wavEmotion in natural language via emotion (or instruction):
curl https://sovrgpt.com/api/v1/audio/speech \
-H "Authorization: Bearer $SOVR_KEY" -H "Content-Type: application/json" \
-d '{
"provider": "cosyvoice",
"voice": "de-thorsten",
"input": "Diese Zeiten sind für immer vorbei.",
"emotion": "Sprich sehr traurig, leise und langsam."
}' --output sad.wavYour own voice (zero-shot cloning): pass a reference clip of 10–20 seconds as base64,
plus its transcript as prompt_text for the best quality:
REF=$(base64 -w0 my_voice.wav)
curl https://sovrgpt.com/api/v1/audio/speech \
-H "Authorization: Bearer $SOVR_KEY" -H "Content-Type: application/json" \
-d "{
\"provider\": \"cosyvoice\",
\"input\": \"A new sentence, spoken in my cloned voice.\",
\"reference_audio_b64\": \"$REF\",
\"prompt_text\": \"<verbatim transcript of the reference clip>\"
}" --output cloned.wavBuilt-in voice:
de-thorsten(German, male, calm; CC0). Your own voices at any time viareference_audio_b64(zero-shot, no training).Cold start: the CosyVoice GPU worker scales to zero. Warm it delivers in about 6 seconds; a cold worker can take minutes. You then get a
503saying “warming up”; resend after about a minute. Mistral (the default) and Supertonic have no cold start.
Choosing the language
The default engine Mistral has no language parameter: the language follows the text.
Supertonic and Qwen speak the language you name in language. Without it, German applies
(English for voice: "default-en"); the text alone does not change that. With
"language": "auto" we detect the language from the text (script and common function
words). If detection is unsure, for example with very short or mixed text, German applies.
curl https://sovrgpt.com/api/v1/audio/speech \
-H "Authorization: Bearer $SOVR_KEY" -H "Content-Type: application/json" \
-d '{"model":"supertonic-3","voice":"F3","language":"fr","input":"Bonjour, je vous lis la réponse."}' \
--output bonjour.wavThe response header X-Voice-Language tells you which language was spoken. A code the
engine does not know is rejected (Supertonic 400, Qwen 422, each with the list
supported), not silently replaced by German. Mistral and CosyVoice have no language
parameter; they speak the language of the text.
Qwen3-TTS: our own voices without Mistral (preliminary)
With provider: "qwen" (or model: "qwen3-tts", or a rooom-* voice), Qwen3-TTS (0.6B,
Apache 2.0) speaks. We run the model ourselves on rented GPUs in EU data centres (and
additionally in Iceland or Norway, EEA); the text goes to no third-party speech service such
as Mistral.
| Voice | Name | Description |
|---|---|---|
rooom-f-01 | Beraterin | female, 30–35, warm, friendly, clear, calm pace |
rooom-f-02 | Fachexpertin | female, 45–50, matter-of-fact, confident, crisp articulation |
rooom-m-01 | Berater | male, 30–35, friendly, open, lively |
rooom-m-02 | Fachexperte | male, 45–50, deep, calm, trustworthy |
The voices are designed and fictional; they belong to no real person. Without voice,
rooom-f-01 speaks.
curl https://sovrgpt.com/api/v1/audio/speech \
-H "Authorization: Bearer $SOVR_KEY" -H "Content-Type: application/json" \
-d '{"provider":"qwen","voice":"rooom-m-01","language":"en","input":"Good morning, here is your summary."}' \
--output qwen.wavPreliminary. The four voices are still waiting for listening approval. Until then their
sound and identifier may change; GET /v1/audio/voices marks them with
status: "preliminary". We have checked German and Russian (round-trip transcription);
there is no measurement of our own for the other eight languages yet.
- Languages:
de en fr es it pt ru ja ko zh. Withoutlanguage,deapplies. Other codes yield422 language_not_supported, Polish for example. - Length: at most 2,000 characters per request, otherwise
400 text_too_long. Split longer texts into paragraphs. - Output: always WAV, 24 kHz, mono, with AI labelling in the metadata. No watermark, no
voice cloning (
reference_audio_b64yields400), no emotion tags. - Cold start: the worker scales to zero. We measured 50 s to the first sound when the
image is already on the machine, and about ten minutes on the very first start on a new
machine. We wait up to 240 s. If it takes longer you get
503; resend after about a minute. If all speech streams are taken you get429withRetry-After: 1.
POST /v1/audio/transcriptions — speech-to-text
Transcribes an uploaded audio file (multipart/form-data).
curl https://sovrgpt.com/api/v1/audio/transcriptions \
-H "Authorization: Bearer $SOVR_KEY" \
-F file=@recording.webm \
-F language=de \
-F response_format=jsonForm fields
| Field | Type | Default | Description |
|---|---|---|---|
file | file | – | Audio (webm/ogg/mp3/wav/m4a …), 4.5 MiB or less, see the note below. Either file or upload_id. |
upload_id | string | – | Identifier from POST /v1/audio/uploads. The audio then does not travel in the request body: up to 100 MiB. See “Large files” below. |
language | string | – | ISO 639-1, e.g. de. Mirrored back in the response. Without it, language in the response stays null, because Voxtral has no automatic language detection. Transcription itself works without it. With model=whisper-large-v3, Whisper detects the language itself and reports it. |
response_format | string | json | json ({ "text": … }), text (plain text), verbose_json ({ text, language, duration }, plus segments when requested). With Whisper also srt and vtt. |
timestamp_granularities | string | – | segment or word: adds a segments field with timestamps to the verbose_json response. Exactly one value; segment,word is rejected with 400. |
context_bias | string | – | Repeatable. Proper nouns and domain terms recognition should be pulled towards, such as product or customer names. A term must contain no space (hyphens are fine); terms with spaces are discarded. At most 100 entries. |
format_text | boolean | false | Additionally formats the transcript: paragraphs, a numbered list from spoken “first/second/third”, a blank line after the salutation, a separated closing. See below. |
model | string | – | whisper-large-v3 selects Whisper large-v3 (see below). Any other value, including whisper-1, and no value mean Voxtral (voxtral-mini-transcribe). No value is rejected; x-sovrgpt-model-served tells you which model transcribed. |
prompt | string | – | Whisper only: sets spellings and style (Whisper's initial_prompt). Only the last roughly 220 tokens take effect. |
The request body is limited to 4.5 MiB. Our platform cuts off request bodies at
4.5 MiB before this endpoint runs. A larger file therefore never reaches us; you get the
platform error FUNCTION_PAYLOAD_TOO_LARGE.
Use Opus or MP3 rather than WAV: 4.5 MiB of Opus at 32 kbit/s is roughly 20 minutes of
speech, uncompressed WAV not even three. For larger files, use upload_id (up to 100 MiB,
next section).
Large files: POST /v1/audio/uploads
The 4.5 MiB is a limit of our platform and cannot be raised in code. So you place the audio past our function: you fetch a signed address, upload straight to it, and at transcription time you only name the identifier.
# 1. Fetch a ticket — same key, same `transcribe` scope
curl -s -X POST https://sovrgpt.com/api/v1/audio/uploads \
-H "Authorization: Bearer $SOVR_KEY" \
-H "content-type: application/json" \
-d '{"mime":"audio/ogg","size":18234012}'{
"upload_id": "0f1c…",
"upload_url": "https://…/storage/v1/object/upload/sign/api-audio/…",
"token": "…",
"content_type": "audio/ogg",
"max_bytes": 104857600
}# 2. Upload the bytes directly — NOT to us
curl -s -X PUT "$upload_url" \
-H "content-type: audio/ogg" \
--data-binary @recording.ogg
# 3. Transcribe, with no file in the body
curl -s -X POST https://sovrgpt.com/api/v1/audio/transcriptions \
-H "Authorization: Bearer $SOVR_KEY" \
-H "content-type: application/json" \
-d '{"upload_id":"0f1c…","language":"de","response_format":"verbose_json","timestamp_granularities":"segment"}'| Field | Type | Description |
|---|---|---|
mime | string | Required. audio/mpeg, audio/mp4, audio/x-m4a, audio/aac, audio/wav, audio/webm, audio/ogg, audio/opus, audio/flac. Any other type is rejected with 415, before the upload. |
size | number | Required. Size in bytes. Above 100 MiB → 413. |
- A ticket is single-use. After transcription the file is deleted, including when the transcription failed. A retry needs a new ticket. That way no audio archive nobody asked for builds up on our side.
- You are billed for the transcription, not for the ticket. A ticket you never redeem costs nothing.
- The ticket route requires the same scope (
transcribe) as transcription itself.
100 MiB is our limit, not our provider's. We have not measured what file size and runtime the recognition service accepts. For very long recordings it may make sense to cut them up; 100 MiB of Opus at 32 kbit/s would be roughly seven hours.
Use context_bias for proper nouns only. The parameter pulls the text towards the
listed terms and distorts correctly recognised everyday words. Example: a listed
“Refactoring” turned a correctly transcribed “refactoren” into “refactoring”.
Response
{ "text": "Hallo, dies ist ein Test." }With response_format=verbose_json and timestamp_granularities:
{
"text": "Die Bundeswehr braucht eine souveräne Plattform. Der letzte Satz ist genau der, den wir prüfen wollen.",
"language": "de",
"duration": 7,
"segments": [
{ "text": "Die Bundeswehr braucht eine souveräne Plattform.", "start": 0.5, "end": 3.3, "speakerId": null },
{ "text": "Der letzte Satz ist genau der, den wir prüfen wollen.", "start": 3.6, "end": 6.9, "speakerId": null }
]
}segments appears in the response only if timestamp_granularities was set; an empty field
would be indistinguishable from “nothing recognised”. The timestamps always refer to the
unformatted text. format_text is a second model pass with its own wording, and there are
no timestamps for it. speakerId is currently always null.
Every successful response carries x-sovrgpt-model-served (voxtral-mini-transcribe or
whisper-large-v3). If your model differs, for example whisper-1, it is echoed in
x-sovrgpt-model-requested.
| Status | Meaning |
|---|---|
| 400 | neither file nor upload_id, or two values in timestamp_granularities |
| 401 / 403 | key or scope missing |
| 404 | upload_id unknown, or the bytes were never uploaded |
| 413 | over 4.5 MiB on the body route |
| 502 | upstream error |
| 503 | transcription not configured in this environment. With Whisper also: the worker is starting up or still loading the model, resend in 1–2 minutes. With upload_id, the ticket is then consumed: upload the audio again (POST /v1/audio/uploads) and send the new upload_id |
| 504 | Whisper: when the wait ran out, the worker had already been processing the recording for at least two minutes; split it. Voxtral: the recognition service did not answer within 270 seconds; send again |
format_text — a transcript with structure
A raw transcript has punctuation and capitalisation, but no paragraphs, no lists and no
blank line after the salutation. With format_text=1 a second pass therefore runs over a
model with audio understanding, which adds that structure. Both passes start at the same
time, so the response time is that of the slower one.
curl https://sovrgpt.com/api/v1/audio/transcriptions \
-H "Authorization: Bearer $SOVR_KEY" \
-F file=@dictation.webm \
-F format_text=1 \
-F response_format=verbose_json{
"text": "Sehr geehrte Frau Dr. Schmitt,\n\nvielen Dank …\n\n1. Die Übertragung …\n2. Die Antwortzeiten …\n\nMit freundlichen Grüßen\nMax Mustermann",
"text_raw": "Sehr geehrte Frau Dr. Schmitt, vielen Dank … Erstens die Übertragung …",
"language": null,
"duration": 101,
"formatting": { "applied": true, "verdict": "ok", "coverage": 0.996 }
}The exact wording is protected. A model asked to format can also rephrase or shorten, and the result still looks flawless. So we check every formatted version against the raw transcript before returning it:
formatting.verdict | Meaning | What you get |
|---|---|---|
ok | Only the structure changed. | the formatted version |
auffaellig | Individual words differ. | the formatted version; read text_raw against it |
— (applied: false) | The wording was altered, or the pass failed. | the raw transcript, plus formatting.reason |
text_raw is always included when format_text=1, even if the formatting was applied. A
client that depends on the literal text reads text_raw and ignores text. format_text
is off by default; existing integrations get exactly the same bytes as before.
Limits of the formatted path. It uses a different model from the raw path and
therefore does not know context_bias, speaker separation or word-level timestamps; those
affect text_raw only. It also processes at most about 20 minutes of audio per request
(the raw path considerably more). For dictation that is plenty; for recordings of long
meetings it is not.
Whisper large-v3 — 99 languages, self-operated
With model=whisper-large-v3, Whisper large-v3 transcribes instead of Voxtral. The model is
self-operated on rented GPUs in EU data centres (and additionally in Iceland or Norway, EEA);
the audio goes to no external speech service.
curl https://sovrgpt.com/api/v1/audio/transcriptions \
-H "Authorization: Bearer $SOVR_KEY" \
-F model=whisper-large-v3 \
-F file=@interview.ogg \
-F response_format=vtt- 99 languages plus Cantonese (
yue). Withoutlanguage, Whisper detects the language itself andverbose_jsonreports it inlanguage;language=automeans the same. Region suffixes such asde-DEare cut tode, and spellings Whisper writes differently are mapped (nb/nb-NO→no,jv→jw,fil→tl, the legacy codesiw,in,ji→he,id,yi). An unknown code returns400. - Response formats:
json,text,verbose_json,srt,vtt. With Whisper,verbose_jsonalways containssegmentsin Whisper's shape (id,start,end,text,avg_logprob,no_speech_prob…); an empty field here really means “no speech detected”.timestamp_granularities=wordaddswords; both levels at once are allowed. durationis the end of the last recognised segment, not the length of the file. Trailing silence does not count. Billing uses this duration.context_biasbecomes thepromptfor Whisper when you send nopromptof your own.format_textis not available with Whisper (400). The formatting pass runs at a different provider; if you choose Whisper so the audio stays with the self-operated model, it must not be sent there without asking.- The default stays Voxtral. Even
model=whisper-1, which OpenAI clients often hard-code, goes to Voxtral. - Large files via
upload_id(up to 100 MiB): the worker then reads the audio itself through a short-lived address from our storage. The ticket is deleted afterwards as always.
Cold start. The Whisper worker scales to zero. The first call after a pause can end with
503 (“warming up”); resend after 1–2 minutes. This also applies when the worker has already
accepted the job and is still loading the model: that job keeps running and warms the model
for your next attempt. With upload_id, the ticket is consumed afterwards: upload the audio
again (POST /v1/audio/uploads) and send the new upload_id; the error message says so too.
We have not
measured the cold-start time yet. A request waits at most about four minutes for the result;
if by then the worker has been processing the recording for at least two minutes, it ends with
504 and you split it. How long a recording may be for that, we have not measured yet either.
whisper-large-v3 appears in GET /v1/models once the model is set up in the environment.
Until then, this endpoint answers 503 for it.
SDK example (OpenAI client)
from openai import OpenAI
client = OpenAI(base_url="https://sovrgpt.com/api/v1", api_key=SOVR_KEY)
# Text-to-speech (Supertonic, fast)
client.audio.speech.create(
model="supertonic-3", voice="F3", response_format="wav",
input="Hallo Welt <sigh> das war ein langer Tag.",
).stream_to_file("hello.wav")
# Text-to-speech (CosyVoice, German + emotion)
# model="cosyvoice-3" picks the engine; emotion (a SovrGPT extension) via extra_body
client.audio.speech.create(
model="cosyvoice-3", voice="de-thorsten", response_format="wav",
input="Was für ein wunderbarer Tag!",
extra_body={"emotion": "Sprich fröhlich und aufgeregt."},
).stream_to_file("happy.wav")
# Speech-to-text
with open("recording.webm", "rb") as f:
tr = client.audio.transcriptions.create(
model="voxtral-mini-transcribe", file=f, language="de",
)
print(tr.text)
# Speech-to-text with Whisper large-v3 (99 languages, language is detected)
with open("interview.ogg", "rb") as f:
tr = client.audio.transcriptions.create(
model="whisper-large-v3", file=f, response_format="verbose_json",
)
print(tr.language, tr.text)In the chat
In the chat, speech works without the API: the microphone icon in the input field dictates (STT), and the speaker button on any assistant answer reads it aloud (TTS). Each user picks the voice under Settings → Voices (from Starter or via entitlement); samples under Voices. See also Models → Voice.
Answers are read aloud in the language of the answer: it is detected from the answer text
(language: "auto"; code blocks and links do not count). If the text is too short or mixed
for that, the interface language applies, otherwise German.
In the chat, the default voice (currently Mistral, Supertonic for other languages), a chosen
catalogue voice or a chosen Supertonic voice reads aloud, all without a cold start.
CosyVoice and Qwen3-TTS run on GPU workers that scale to zero, so they are only available
through POST /v1/audio/speech.