Skip to content
docs/audioCore API

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

AUTHENTICATION
Bearer token, Authorization header
REQUIRED SCOPE
api-key

Speech parameters

PARAMETERTYPEREQUIREDDESCRIPTION
modelstringrequiredA slug from GET /v1/models whose category is audio.
inputstringrequiredThe text to speak, up to 4,096 characters. This is the billable quantity.
voicestringoptionalVendor voice id. Default alloy. Not an enum. A new voice needs no release from us.
response_formatmp3 | opus | aac | flac | wav | pcmoptionalContainer for the returned audio. Omitted leaves the model's default, which is usually mp3.
speednumberoptionalPlayback rate, 0.25 to 4.0. Forwarded rather than clamped here.

The speech response

HTTP200 OK
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
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.

AUTHENTICATION
Bearer token, Authorization header
REQUIRED SCOPE
api-key

Transcription parameters

PARAMETERTYPEREQUIREDDESCRIPTION
modelstringrequiredA slug from GET /v1/models whose category is audio.
input_audioobjectrequired{ data, format }. Base64 audio, bare or with a data: prefix, and the container it is in.
languagestringoptionalISO-639-1 hint. Improves accuracy and latency where the language is known.
response_formatjson | text | srt | verbose_json | vttoptionalShape of the transcript. Default json.
temperaturenumberoptionalSampling temperature, 0 to 1. Lower is more literal.
timestamp_granularitiesstring[]optionalword and/or segment. Only meaningful alongside verbose_json.

The transcription response

JSON200 OK
{
  "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

STATUSCODEMEANING
400invalid_requestNot an audio model, an input over 4,096 characters, or input_audio that is empty or not base64
402insufficient_creditsRefused before any upstream call
409model_unavailableNo live endpoint can serve that audio model
503upstream_unavailableUpstream outage. Safe to retry