Audio
Text to speech and speech to text, in OpenAI's shape.
Two routes, pointing opposite ways. POST /v1/audio/speech takes text and answers with audio bytes; POST /v1/audio/transcriptions takes audio and answers with text. Both take OpenAI's fields and return OpenAI's answer, so an existing client reaches them with nothing changed but the base URL and the model slug, with one exception on the transcription route, which is called out below.
Speech
The response is the audio itself, streamed rather than buffered, so a long document does not sit in memory on either side. Content-Type is the upstream's own (audio/mpeg for an MP3), and X-Content-Type-Options: nosniff rides with it. voice is passed through rather than validated here, so a voice a vendor adds is usable the day it ships instead of the day we notice; an unknown one is refused upstream with a message naming the valid set.
Generating speech
curl https://api.routehook.ai/v1/audio/speech \
-H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/tts-1",
"input": "Routehook audio is live.",
"voice": "alloy"
}' \
--output speech.mp3
POST/v1/audio/speechAvailable
Turn text into spoken audio and stream the bytes back.
Speech parameters
| PARAMETER | TYPE | REQUIRED | DESCRIPTION |
|---|---|---|---|
| model | string | required | A slug from GET /v1/models whose category is audio. |
| input | string | required | The text to speak, up to 4,096 characters. This is the billable quantity. |
| voice | string | optional | Vendor voice id. Default alloy. Not an enum. A new voice needs no release from us. |
| response_format | mp3 | opus | aac | flac | wav | pcm | optional | Container for the returned audio. Omitted leaves the model's default, which is usually mp3. |
| speed | number | optional | Playback rate, 0.25 to 4.0. Forwarded rather than clamped here. |
The speech response
HTTP/1.1 200 OK
Content-Type: audio/mpeg
X-Content-Type-Options: nosniff
X-Routehook-Request-Id: req_7c41d9be
X-Routehook-Cost: 0.00045
<binary audio>
Transcription
Send the audio as base64 under input_audio, with or without a data: prefix, both are accepted, because a blob assembled by hand and one produced by a browser's FileReader differ only in that prefix. format names the container and is worth sending: the upstream decides how to decode from the filename, so a WAV labelled mp3 is rejected by the decoder rather than by us.
Transcribing audio
curl https://api.routehook.ai/v1/audio/transcriptions \
-H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"openai/gpt-4o-transcribe\",
\"input_audio\": { \"data\": \"$(base64 -w0 speech.mp3)\", \"format\": \"mp3\" }
}"
POST/v1/audio/transcriptionsAvailable
Transcribe audio to text, with optional word and segment timestamps.
Transcription parameters
| PARAMETER | TYPE | REQUIRED | DESCRIPTION |
|---|---|---|---|
| model | string | required | A slug from GET /v1/models whose category is audio. |
| input_audio | object | required | { data, format }. Base64 audio, bare or with a data: prefix, and the container it is in. |
| language | string | optional | ISO-639-1 hint. Improves accuracy and latency where the language is known. |
| response_format | json | text | srt | verbose_json | vtt | optional | Shape of the transcript. Default json. |
| temperature | number | optional | Sampling temperature, 0 to 1. Lower is more literal. |
| timestamp_granularities | string[] | optional | word and/or segment. Only meaningful alongside verbose_json. |
The transcription response
{
"text": "Routehook audio is live."
}
The other response formats are the vendor's own
Only json is a shape we publish. verbose_json (the one that carries duration, segments and word timings) is forwarded from the upstream exactly as it arrived, and text, srt and vtt come back as the file itself under text/plain; charset=utf-8 rather than as a quoted string. Normalising them would mean guessing at fields for every model discovered after the code was written, so they are passed through instead. Parse json if you want a stable shape; ask for the others when you want the vendor's.
Which models can do this
GET /v1/models reports a category of audio for every model on these two routes. The category tells you the route but not the direction (a model that transcribes cannot speak, and the reverse is also true), so read the model's own name for that. Naming a chat or an image model on either route is refused with 400 invalid_request before any upstream call is made and before any credit is reserved, and the message names the category the model actually is. A target that could otherwise serve the model but does not do audio is skipped by the router rather than called.
Cost
Speech reserves the exact charge before the call: the characters you sent at the model's per-character rate, plus the model's flat per-request price where it carries one. Nothing is estimated, because you have already handed over the thing being counted. Transcription cannot work that way (the billable quantity is the duration of the audio, which nobody knows until the upstream reports one), so the hold is sized from the upload, deliberately high, and replaced by the true charge seconds later. Where no duration is reported the estimate stands and the request row records that the figure was estimated rather than metered. Either way the reservation is released and nothing is charged if the call fails.
Failures
| STATUS | CODE | MEANING |
|---|---|---|
| 400 | invalid_request | Not an audio model, an input over 4,096 characters, or input_audio that is empty or not base64 |
| 402 | insufficient_credits | Refused before any upstream call |
| 409 | model_unavailable | No live endpoint can serve that audio model |
| 503 | upstream_unavailable | Upstream outage. Safe to retry |