SovrGPT Docs
API

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.

providerStrengthVoicesLanguagesEmotion / tagsCloningFormat
supertonicfast, no cold startM1–M5 / F1–F531, chosen via language<breath>/<sigh>–wav/flac
cosyvoiceGerman with emotion and cloningde-thorsten or your own voice9, follows the textfull inline tags plus natural-language emotionyes (zero-shot)wav (24 kHz)
mistral (default)Default engine and voice catalogue: designed German voicesbib-m-40, bib-f-35, … (see Voices)9, follows the text––mp3/wav/opus/flac
qwenour own voices without Mistral, preliminaryrooom-f-01, rooom-f-02, rooom-m-01, rooom-m-0210, 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.wav

Request body

FieldTypeApplies toDescription
inputstringallRequired. The text to speak, 4,000 characters or fewer (2,000 for qwen). Inline tags depend on the provider (see below).
voicestringallCatalogue 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.
providerstringallsupertonic | cosyvoice | mistral | qwen. Optional; otherwise derived from model/voice, otherwise the server default (currently mistral).
response_formatstringSupertonic/Mistralwav/flac (Supertonic; any other value yields wav), plus mp3/opus/aac (Mistral). CosyVoice and Qwen always return wav.
languagestringSupertonic/QwenThe 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 / instructionstringCosyVoiceNatural-language style or emotion instruction, e.g. "Sprich sehr traurig und langsam."
reference_audio_b64stringCosyVoiceBase64 wav/flac (30 s or less): clones that voice (zero-shot). Overrides voice. With provider: qwen you get 400 cloning_not_supported.
prompt_textstringCosyVoiceTranscript of the reference clip (best quality). Automatic for built-in voices.
speednumberCosyVoice0.5–2.0 (default 1.0). Supertonic/Mistral/Qwen: accepted, currently ignored.
modelstringallSelects 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.

StatusMeaning
400input 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 / 403key 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).
422language_not_supported (qwen): the language is not one of the ten Qwen languages, see supported.
429busy (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.
502upstream error; for qwen also a speech stream that broke off early (stream_incomplete). We never deliver half an announcement.
503TTS 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 pins mistral, a rooom-* voice pins qwen.
  • selectable: whether this organisation may pick the voice. Catalogue voices depend on the plan (see access); 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 four rooom-* 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.
  • languages stays the list of the Mistral voices for compatibility; languages_by_provider lists 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.
  • languages per 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: false means POST /v1/audio/speech answers 403 voice_not_in_plan for catalogue voices. via names the reason for access (tier from Starter, entitlement via the “Voices” entitlement).
  • samples are 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 public samples, because the sample route is reachable without signing in. Instead they carry a signed sample_url valid 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.mp3

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

MethodPathWhat it does
GET/v1/audio/voices/design-optionsThe casting sheet: traits, limits, whether it is switched on
POST/v1/audio/voices/designsOrder a design (202, keeps running)
GET/v1/audio/voices/designsThis organisation's designs
GET/v1/audio/voices/designs/{id}Fetch the state; drives the job forward
POST/v1/audio/voices/designs/{id}/adoptClone and register in the catalogue
DELETE/v1/audio/voices/designs/{id}Discard, delete the reference clip
POST/v1/audio/voices/designs/suggestPropose 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).

HTTPcodeMeaning
400invalid_request_errorunknown trait id, age outside 16–90, invalid slug
402quota_exceededspend limit of the organisation or user reached
403voice_not_in_planplan below Starter and no “Voices” entitlement
403voice_role_requiredthe key's creator is not an owner or admin of the organisation
409voice_limit_reachedmaximum own voices reached (or slug taken)
409too_many_pendingtoo many designs running at once
501not_implementedthe design service is not switched on in this environment
502upstream_errorworker 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.wav

Emotion 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.wav

Your 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.wav

Built-in voice: de-thorsten (German, male, calm; CC0). Your own voices at any time via reference_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 503 saying “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.wav

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

VoiceNameDescription
rooom-f-01Beraterinfemale, 30–35, warm, friendly, clear, calm pace
rooom-f-02Fachexpertinfemale, 45–50, matter-of-fact, confident, crisp articulation
rooom-m-01Beratermale, 30–35, friendly, open, lively
rooom-m-02Fachexpertemale, 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.wav

Preliminary. 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. Without language, de applies. Other codes yield 422 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_b64 yields 400), 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 get 429 with Retry-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=json

Form fields

FieldTypeDefaultDescription
filefile–Audio (webm/ogg/mp3/wav/m4a …), 4.5 MiB or less, see the note below. Either file or upload_id.
upload_idstring–Identifier from POST /v1/audio/uploads. The audio then does not travel in the request body: up to 100 MiB. See “Large files” below.
languagestring–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_formatstringjsonjson ({ "text": … }), text (plain text), verbose_json ({ text, language, duration }, plus segments when requested). With Whisper also srt and vtt.
timestamp_granularitiesstring–segment or word: adds a segments field with timestamps to the verbose_json response. Exactly one value; segment,word is rejected with 400.
context_biasstring–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_textbooleanfalseAdditionally formats the transcript: paragraphs, a numbered list from spoken “first/second/third”, a blank line after the salutation, a separated closing. See below.
modelstring–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.
promptstring–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"}'
FieldTypeDescription
mimestringRequired. 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.
sizenumberRequired. 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.

StatusMeaning
400neither file nor upload_id, or two values in timestamp_granularities
401 / 403key or scope missing
404upload_id unknown, or the bytes were never uploaded
413over 4.5 MiB on the body route
502upstream error
503transcription 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
504Whisper: 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.verdictMeaningWhat you get
okOnly the structure changed.the formatted version
auffaelligIndividual 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). Without language, Whisper detects the language itself and verbose_json reports it in language; language=auto means the same. Region suffixes such as de-DE are cut to de, and spellings Whisper writes differently are mapped (nb/nb-NO → no, jv → jw, fil → tl, the legacy codes iw, in, ji → he, id, yi). An unknown code returns 400.
  • Response formats: json, text, verbose_json, srt, vtt. With Whisper, verbose_json always contains segments in Whisper's shape (id, start, end, text, avg_logprob, no_speech_prob …); an empty field here really means “no speech detected”. timestamp_granularities=word adds words; both levels at once are allowed.
  • duration is the end of the last recognised segment, not the length of the file. Trailing silence does not count. Billing uses this duration.
  • context_bias becomes the prompt for Whisper when you send no prompt of your own.
  • format_text is 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.

Audio API (TTS & STT)