sipgate-ai STT

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.

Endpoints

MethodPathAuthWhat
GET/healthznoLiveness check
GET/v1/modelsyesList available models
POST/v1/audio/transcriptionsyesOpenAI-style batch transcription
POST/v1/speech-to-textyesElevenLabs-style batch transcription
WS/v1/realtimeyesOpenAI-style realtime streaming
WS/v1/speech-to-text/realtimeyesElevenLabs-style realtime streaming

Authentication

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.

Models

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.

Which one to pick

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:

AliasLive partialsWord timestampsLanguage
qwen3-asr
default
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.

Realtime streaming (WebSocket)

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 style

Client → server (JSON text frames):

Server → client (JSON):

WS /v1/speech-to-text/realtime — ElevenLabs Realtime style

Connection config is passed as query parameters:

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.

Custom vocabulary

Product names, street names, proper nouns. Works on every engine, batch and realtime, through the field your SDK already has:

SurfaceWhere the terms go
OpenAI batchprompt — comma/newline-separated
ElevenLabs batchkeyterms — JSON array, CSV, or repeated field
OpenAI realtimesession.input_audio_transcription.prompt (also mid-session)
ElevenLabs realtimekeyterms 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 MeierMeyer and Königs alleeeKönigsallee). Consequences worth designing around:

Errors and backpressure

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.

codeMeaningWhat a client should do
nemotron_slots_exhaustedConcurrency cap reachedRetry with backoff, or fall back to another engine. Arrives as the first and only message, then the socket closes
nemotron_not_readyModel still loadingRetry shortly
invalid_json / invalid_control_messageMalformed client frameFix the client — retrying won't help
decode_failedDecoder raised on this sessionReconnect; the session is gone
internal_errorUnexpected server errorReconnect, tell us if it repeats

Compatibility — honoured, ignored, refused

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.

Accepted and deliberately ignored

These do not fail and do not do anything. Don't infer behaviour from a 200:

Refused with 501

Partially honoured

Quick examples

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

API documentation