Speech-to-text API. OpenAI- and ElevenLabs-compatible interfaces on the same key — point whichever SDK you already use at this host.
👉 Interactive playground — try batch & realtime transcription in the browser, no setup.
| Method | Path | Auth | What |
|---|---|---|---|
| GET | /healthz | no | Liveness check |
| GET | /v1/models | yes | List available models |
| POST | /v1/audio/transcriptions | yes | OpenAI-style batch transcription |
| POST | /v1/speech-to-text | yes | ElevenLabs-style batch transcription |
| WS | /v1/realtime | yes | OpenAI-style realtime streaming |
| WS | /v1/speech-to-text/realtime | yes | ElevenLabs-style realtime streaming |
One API key works for both protocols and all endpoints (except
/healthz). Send via whichever header your client library
already speaks:
Authorization: Bearer <key> # OpenAI SDKs
xi-api-key: <key> # ElevenLabs SDKs
WebSocket clients that can't set custom headers (browsers) also accept
the key via the Sec-WebSocket-Protocol subprotocol entry
(bearer.<key>) or the ?api_key= /
?token= query parameter.
Pass the model identifier in the model (OpenAI) or
model_id (ElevenLabs) field. Authenticated clients can
list the available identifiers via /v1/models.
Provider-default names from the OpenAI and ElevenLabs SDKs are accepted
without modification.
Three engines run side by side. They differ in ways that change how you integrate, not just in quality — pick on this table, then compare transcription quality yourself in the playground:
| Alias | Live partials | Word timestamps | Language |
|---|---|---|---|
qwen3-asrdefault |
emulated — ~1 s | estimated (median error 0.24 s) | language hint honoured |
parakeet |
emulated — ~1 s | estimated (median error 0.89 s) | auto-detected; no hint (drifts on short onsets) |
nemotron |
yes — true incremental | from the model (exact) | language used as a prompt |
Read the partials column before you design a live UI. Only
Nemotron streams real incremental partials — it keeps its encoder state across
chunks, so a partial costs almost nothing and arrives within ~100 ms. The two
vLLM-served engines emulate streaming by re-decoding the whole buffer on a
timer, roughly once a second: you do get partial_transcript /
….delta frames, but expect ~1 s granularity, and expect the
gap to widen on a long utterance that never commits (the interval scales with the
buffer, because the decode cost does too). A partial can also be revised, not just
extended — a re-decode may reword what it said before. For word-by-word live
UX use Nemotron.
Unknown model names do not fail — they fall back to the default engine.
That is what makes whisper-1 and scribe_v2 work
unmodified, and it also means a typo silently gets you the default instead of the
engine you meant.
Two bidirectional WebSocket surfaces — the client streams PCM audio up while the server streams transcripts back. These are not in the Swagger/OpenAPI spec: OpenAPI cannot describe WebSocket endpoints. Both pick a backend from the requested model the same way the batch endpoints do, and authenticate with the same key (see above).
WS /v1/realtime — OpenAI Realtime styleClient → server (JSON text frames):
input_audio_buffer.append — {"audio": "<base64 PCM16LE>"}input_audio_buffer.commit (or response.create) — finalise the current utterancesession.update — optional language hint mid-session; also carries
per-session custom vocabulary via
session.input_audio_transcription.prompt (see
Custom vocabulary)Server → client (JSON):
session.createdconversation.item.input_audio_transcription.delta — {"delta": "…"} (incremental)input_audio_buffer.committed then
conversation.item.input_audio_transcription.completed — {"transcript": "…"}errorWS /v1/speech-to-text/realtime — ElevenLabs Realtime styleConnection config is passed as query parameters:
model_id — backend selector (optional)encoding / audio_format — must be pcm_<rate> (default pcm_16000)sample_rate — default 16000 (vLLM backends require 16000)commit_strategy — manual (default) or vad (server endpoints on silence)language_code — optional hintinclude_timestamps — true/false. Word times are
session-absolute (they keep running across commits). Nemotron aligns them on the
model; the vLLM backends (Qwen3-ASR, Parakeet) approximate them —
session_started.config.timestamps_source says which you got
(model / estimated).timestamps_offset_secs — added to every word time, e.g. to place one
leg of a call in the recording's timebase. Any finite value, |value| ≤ 86400.commit_strategy=vad): vad_threshold,
min_speech_duration_ms, vad_silence_threshold_secskeyterms — per-session custom vocabulary, repeatable
(&keyterms=Meyer&keyterms=Königsallee), CSV or JSON array
(see Custom vocabulary)Client → server (JSON): {"audio_base_64": "<base64 PCM>"} to stream audio,
{"commit": true} to finalise (when manual).
Server → client (JSON, discriminated by message_type):
session_started, partial_transcript {"text": "…"},
committed_transcript (or committed_transcript_with_timestamps) {"text": "…"},
error.
Product names, street names, proper nouns. Works on every engine, batch and realtime, through the field your SDK already has:
| Surface | Where the terms go |
|---|---|
| OpenAI batch | prompt — comma/newline-separated |
| ElevenLabs batch | keyterms — JSON array, CSV, or repeated field |
| OpenAI realtime | session.input_audio_transcription.prompt (also mid-session) |
| ElevenLabs realtime | keyterms connect query param |
curl -X POST https://stt.sipgate.ai/v1/audio/transcriptions \
-H "Authorization: Bearer $KEY" \
-F file=@sample.wav -F model=parakeet \
-F prompt="Meyer, Königsallee, ChargeBee"
It corrects the transcript, it does not steer the decoder. The model transcribes first, then near-misses are rewritten toward your terms (edit distance plus German phonetics, so Meier → Meyer and Königs alleee → Königsallee). Consequences worth designing around:
Königsallee over Allee — short generic terms
capture ordinary words that merely sound similar.200 with fewer terms applied.Realtime errors arrive as a JSON frame with a stable code.
Branch on code, never on message — the
text is for humans and proxies may rewrite it.
code | Meaning | What a client should do |
|---|---|---|
nemotron_slots_exhausted | Concurrency cap reached | Retry with backoff, or fall back to another engine. Arrives as the first and only message, then the socket closes |
nemotron_not_ready | Model still loading | Retry shortly |
invalid_json / invalid_control_message | Malformed client frame | Fix the client — retrying won't help |
decode_failed | Decoder raised on this session | Reconnect; the session is gone |
internal_error | Unexpected server error | Reconnect, tell us if it repeats |
The interfaces are OpenAI- and ElevenLabs-compatible, which means SDKs send fields we have no engine for. Nothing is silently half-done; every field is in one of three states. Fields not listed here are honoured.
These do not fail and do not do anything. Don't infer behaviour from a
200:
diarize, num_speakers — no backend diarizes; every
word comes back as speaker_0tag_audio_events — no audio-event taggingadditional_formats — only the default JSON shape is producedtemperature — not forwarded to any backenduse_multi_channel — audio is always treated as single-channelenable_logging — request logs only ever record a key fingerprint,
never audio, so there is nothing to switch offwebhook_metadata — webhooks are unsupported (see below)501webhook=true — no webhook delivery; poll the response insteadcloud_storage_url — upload the audio in the requesttimestamps_granularity — none vs. anything else is
honoured; character granularity is accepted but you get
word timestamps/v1/audio/transcriptions and WS /v1/realtime return
no words array (neither does the real OpenAI API in this form).
Needing timestamps means using the ElevenLabs endpoints — the key and the
engines are the same.OpenAI-style batch:
curl -X POST https://stt.sipgate.ai/v1/audio/transcriptions \
-H "Authorization: Bearer $KEY" \
-F file=@sample.wav \
-F language=de \
-F response_format=verbose_json
ElevenLabs-style batch with raw PCM (skips server-side ffmpeg, lower latency):
ffmpeg -i sample.wav -f s16le -ac 1 -ar 16000 sample.pcm
curl -X POST https://stt.sipgate.ai/v1/speech-to-text \
-H "xi-api-key: $KEY" \
-F file=@sample.pcm \
-F file_format=pcm_s16le_16 \
-F language_code=deu
WebSocket streaming (ElevenLabs flavour, with
websocat):
websocat -t "wss://stt.sipgate.ai/v1/speech-to-text/realtime?\
model_id=nemotron&encoding=pcm_16000&sample_rate=16000&\
commit_strategy=MANUAL&language_code=deu&\
include_timestamps=true&keyterms=Königsallee&token=$KEY"
(model_id=nemotron because this example asks for live partials and
exact word times — see Which one to pick.)