Everything on this page is defined at the HTTP level, so it applies equally to Java, Go, C#, Rust, PHP, Node.js, Python, or a hand-rolled HTTP client. No SDK is required and no WebSocket client is required. Every claim below is reproducible with the commands given; the verification section can be run before you hold an API key.
Status, measured on production 2026-07-29. A realtime WebSocket channel is now partially live: POST /v1/realtime/sessions issues a session token for openai/gpt-realtime-whisper, but connecting to wss://api.therouter.ai/v1/realtime with that token currently answers 503 upstream_unavailable ("This model is not configured for realtime billing"). Until that clears, the working speech-to-text path is the ordinary HTTP endpoint this guide documents, and everything below is verified against it:
POST https://api.therouter.ai/v1/audio/transcriptions
Content-Type: multipart/form-dataIf you are porting a client that was written against OpenAI's Realtime API, the WebSocket session handshake and every /v1/realtime/* session-creation call must be removed, not merely repointed. They are answered with a deliberate 501 β see the next section for the exact contract and why it is not a misconfiguration on your side.
The sub-paths TheRouter does not implement β POST /v1/realtime/transcription_sessions, POST /v1/realtime/calls, POST /v1/realtime/client_secrets, and anything else under /v1/realtime/ other than the two real routes above β return HTTP 501 Not Implemented with a stable machine-readable body. They are registered deliberately, so that a client falling back from one realtime sub-path to another gets this instead of a routing 404 that reads like a mistyped URL:
{
"error": {
"message": "Realtime WebSocket sessions are not available on TheRouter. Use POST /v1/audio/transcriptions with model 'openai/gpt-realtime-whisper' for near-realtime transcription instead.",
"type": "api_error",
"code": "realtime_websocket_not_supported",
"param": null,
"guidance": "Use POST /v1/audio/transcriptions (model openai/gpt-realtime-whisper), chunking audio client-side for near-realtime use.",
"docs": "https://therouter.ai/guides/multimodal/realtime-transcription"
}
}Three properties of this response are contractual and you may rely on them:
GET /v1/realtime and POST /v1/realtime/sessions β do authenticate, and answer 401 without a valid key.)404 model_not_found there./v1/realtime/ subtree is registered specifically so that a fallback to a different realtime sub-path cannot produce a routing 404 that reads like a mistyped URL.Branch on error.code, not on the HTTP status. The status may be rewritten by an intermediate proxy or CDN; code is the stable contract. A client that keys on realtime_websocket_not_supported will keep working unchanged if the realtime channel is implemented later β its handshake will simply start succeeding.
There is now, and it is partially live. The honest answer has three parts, because the rollout is mid-flight:
POST /v1/realtime/sessions with {"model":"openai/gpt-realtime-whisper"} and a valid key returns 200 with a session_token and an expires_at. Without a key it answers 401, not 501.openai/gpt-realtime-2.1 and openai/gpt-realtime-2.1-mini serve full speech-to-speech sessions over wss://api.therouter.ai/v1/realtime?model=...: audio in, audio and transcript deltas out, usage billed per session. Verified against live OpenAI and Azure upstreams on both routes.openai/gpt-realtime, -mini, -1.5, -2 and -translate answer 404 model_not_found. They are not typos and not a permissions problem β they have not been through the same verification, and a 404 is how that is stated rather than letting a request fail somewhere downstream.gpt-realtime-whisper is transcription-only and stays on HTTP. It is not a conversational model; use POST /v1/audio/transcriptions for it, as the rest of this guide describes. Sending it to /v1/chat/completions has no route and answers 503.Which path to build on. For speech-to-text, build on POST /v1/audio/transcriptions β it is the whole subject of this guide and it does not require a WebSocket client. For two-way spoken conversation, use the realtime socket with one of the two models named above.
Your first session.update must carry session.type. Upstream added it as a required parameter, and a payload written against the older shape is answered with missing_required_parameter for session.type β the session opens, then nothing you send is accepted. This is upstream's contract, relayed verbatim; TheRouter does not rewrite your frames.
{
"type": "session.update",
"session": {
"type": "realtime",
"output_modalities": ["audio"],
"instructions": "You are a concise voice assistant.",
"audio": {
"input": { "format": { "type": "audio/pcm", "rate": 24000 }, "turn_detection": null },
"output": { "format": { "type": "audio/pcm", "rate": 24000 }, "voice": "alloy" }
}
}
}Authenticate the handshake with an ordinary Authorization: Bearer header. Browsers cannot set headers on a WebSocket handshake, so for browser clients mint a short-lived token with POST /v1/realtime/sessions and pass it as ?session_token= instead. Tokens are single-use and short-lived.
The confusion that brought most readers here is still worth naming, because the vendor documentation caused it and the vendor documentation was not wrong. gpt-realtime-whisper is not reachable over the provider's own REST transcription endpoint at all β it is rejected by model name there, and a realtime session is its only serving path at the vendor. TheRouter speaks that upstream WebSocket server-side and presents the result through the ordinary REST transcription contract, which is why the same model can be a WebSocket-only model at the vendor and an ordinary HTTP call here.
Streaming does exist on TheRouter, but in the other direction. Text-to-speech (POST /v1/audio/speech) supports opt-in streaming via stream_format: "sse" (Server-Sent Events) or stream: true (chunked binary). Both are plain HTTP, not WebSocket. There is no streaming counterpart for speech-to-text today: transcription returns one complete JSON body per request, which is why near-realtime output has to be built by segmenting on the client.
Run these now. They isolate transport and routing from credentials, and they establish what your account actually sees before you write any code β the realtime rollout means the answers differ by path.
# 1. No key needed: an unimplemented realtime sub-path answers 501 with a
# machine-readable code β not a bare 404 that would look like a typo.
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST https://api.therouter.ai/v1/realtime/transcription_sessions \
-H "Content-Type: application/json" -d '{"model":"openai/gpt-realtime-whisper"}'
# expect: 501 (body carries "code":"realtime_websocket_not_supported")
# 2. Session creation IS implemented β without a key it answers 401, not 501.
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST https://api.therouter.ai/v1/realtime/sessions \
-H "Content-Type: application/json" -d '{"model":"openai/gpt-realtime-whisper"}'
# expect: 401
# 3. The path this guide builds on. With your key, end to end:
curl https://api.therouter.ai/v1/audio/transcriptions \
-H "Authorization: Bearer $THEROUTER_API_KEY" \
-F file="@speech.wav" -F model="openai/gpt-realtime-whisper"
# expect: 200 with {"text": "...", "duration": N}If step 3 succeeds you are done β that is the whole dependency of everything below. A connection failure, a 403 from an intermediary, or a captive-portal redirect on any of the three is a network-path problem between you and the API, and should be resolved before continuing.
A single multipart/form-data POST. Field names and semantics match the OpenAI transcription API, so an existing OpenAI-compatible client works after changing only the base URL and the model name.
| Field | Required | Type | Notes |
|---|---|---|---|
file | Yes | binary part | The audio. The part must carry a filename with a real extension (e.g. chunk.wav) β format is inferred from it. A filename-less part is the single most common integration failure. |
model | Yes | string | openai/gpt-realtime-whisper, openai/gpt-4o-transcribe, or openai/whisper-1. |
language | No | ISO-639-1 | e.g. en, zh. Supplying it improves accuracy and reduces latency; omitting it auto-detects. |
prompt | No | string | Biases decoding toward domain vocabulary, proper nouns, and spellings. |
response_format | No | string | json (default) or text. |
temperature | No | number | 0 for maximum determinism. |
Headers: Authorization: Bearer <THEROUTER_API_KEY>. Do not set Content-Type by hand β your HTTP library must set it, because it has to append the multipart boundary.
Accepted container formats: WAV, MP3, FLAC, M4A, MPEG, MPGA, OGG, WebM. Maximum request size: 25 MB. 16 kHz mono is sufficient for speech; higher sample rates increase payload size without improving accuracy.
HTTP/1.1 200 OK
Content-Type: application/json
{
"text": "Hello, this is a test of the The Router audio transcription pipeline. Today is June first. Let's see if it works.",
"duration": 7,
"model": "openai/gpt-realtime-whisper"
}duration is the billable audio length in seconds. It is the quantity your charge is derived from β see Billing.
All four send the identical HTTP request. Pick whichever matches your stack; none of them requires a vendor SDK.
curl https://api.therouter.ai/v1/audio/transcriptions \
-H "Authorization: Bearer $THEROUTER_API_KEY" \
-F file="@speech.wav" \
-F model="openai/gpt-realtime-whisper" \
-F language="en" \
-F response_format="json"import okhttp3.*;
import java.io.File;
import java.io.IOException;
public final class TheRouterTranscriber {
private static final String ENDPOINT =
"https://api.therouter.ai/v1/audio/transcriptions";
private static final String MODEL = "openai/gpt-realtime-whisper";
private final OkHttpClient client = new OkHttpClient();
private final String apiKey;
public TheRouterTranscriber(String apiKey) {
this.apiKey = apiKey;
}
public String transcribe(File audio, String language) throws IOException {
// The filename on this part is REQUIRED β the audio format is inferred
// from its extension. Passing a nameless part is the most common cause
// of a 400 here.
MultipartBody body = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", audio.getName(),
RequestBody.create(audio, MediaType.parse("audio/wav")))
.addFormDataPart("model", MODEL)
.addFormDataPart("language", language)
.addFormDataPart("response_format", "json")
.build();
Request request = new Request.Builder()
.url(ENDPOINT)
.addHeader("Authorization", "Bearer " + apiKey)
// Do NOT set Content-Type: OkHttp must add the multipart boundary.
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
String payload = response.body() != null ? response.body().string() : "";
if (!response.isSuccessful()) {
throw new IOException(
"transcription failed: HTTP " + response.code() + " " + payload);
}
return payload; // {"text": "...", "duration": 7, "model": "..."}
}
}
}package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func Transcribe(apiKey, path, language string) (string, error) {
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
// CreateFormFile sets the filename, which the API needs to infer the format.
part, err := w.CreateFormFile("file", "speech.wav")
if err != nil {
return "", err
}
if _, err := io.Copy(part, f); err != nil {
return "", err
}
w.WriteField("model", "openai/gpt-realtime-whisper")
w.WriteField("language", language)
w.WriteField("response_format", "json")
w.Close()
req, err := http.NewRequest("POST",
"https://api.therouter.ai/v1/audio/transcriptions", &buf)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", w.FormDataContentType()) // includes the boundary
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
}
return string(body), nil
}from openai import OpenAI
client = OpenAI(
api_key=os.environ["THEROUTER_API_KEY"],
base_url="https://api.therouter.ai/v1",
)
with open("speech.wav", "rb") as f:
result = client.audio.transcriptions.create(
model="openai/gpt-realtime-whisper",
file=f,
language="en",
)
print(result.text)Because there is no streaming channel, "live" transcription is built by segmenting audio on the client and submitting each segment as an independent request. The pattern is the same in every language:
Requests are independent, so segments may be sent concurrently. If you do, tag each with its sequence number and reorder on receipt, because responses will not arrive in order.
Measured against production on 2026-07-29 with a 6.7-second sample at 3-second segments:
[chunk 0 Β· 6877ms] Hello, this is a test of the the router audio transcription
[chunk 1 Β· 2621ms] pipeline. Today is June first. Let's see
[chunk 2 Β· 1728ms] See if it works.
--- assembled ---
Hello, this is a test of the the router audio transcription pipeline.
Today is June first. Let's see See if it works.This is an approximation of streaming, not an equivalent to it. Evaluate it against your accuracy budget before committing. Two limits are inherent to the approach and visible in the run above:
Let's see / See if it works is the same speech transcribed twice, because the boundary fell mid-phrase. Each request is stateless and carries no context from its predecessor. Mitigate with 3β5 second segments, boundaries placed at silence via client-side VAD rather than on a fixed timer, and de-duplication of the overlap between consecutive results. For accuracy-critical work, submit the complete recording in one request instead.Transcription is billed per minute of audio processed, derived from the duration field of each response β not per request. Ten 6-second segments and one 60-second recording cost the same, so client-side segmentation does not change your bill beyond rounding.
For the current per-minute rate, see the model page: openai/gpt-realtime-whisper. It is the authoritative rate card; prices are not restated here so that this guide cannot drift out of date.
| Realtime setting | Action |
|---|---|
websocket-url: wss://.../v1/realtime | Remove. Not repoint β there is no WebSocket URL to move it to. |
POST /v1/realtime/sessions, /v1/realtime/transcription_sessions | Remove. There is no session object; each HTTP request is self-contained. |
commit-interval-ms, delay, VAD/turn-detection settings | Remove; reimplement as your client-side segment length. |
input_audio_format (raw PCM) | Replace. Wrap PCM in a container (WAV) per segment; raw PCM is not accepted. |
http-base-url: https://api.therouter.ai/v1 | Keep. |
model: openai/gpt-realtime-whisper | Keep. |
| Status | code | Cause and resolution |
|---|---|---|
| 501 | realtime_websocket_not_supported | A /v1/realtime path was called. Not a credential or configuration fault. Migrate to POST /v1/audio/transcriptions. |
| 400 | invalid_audio_format | Unreadable container, or a file part with no filename/extension. Verify each segment decodes standalone before sending. |
| 401 | β | Missing or invalid key. Note this cannot be the cause of a 501, which is returned before authentication. |
| 413 | β | Request above 25 MB. Shorten the segment or lower the sample rate. |
| 429 | β | Rate limited. Back off exponentially; lower segment concurrency. |