The speech-to-text call at the front of your pipeline now runs on the OpenRouter key you already have. Same model, same 19 languages, our published rate with no markup.
A voice agent captures a turn the moment endpointing fires, and nothing downstream can start until the words come back. A dictation feature has to put text on screen while the user is still holding the key down. A phone tree captures one utterance and branches the call on what it heard. A push-to-talk message leaves the moment the user lifts their thumb. In all of them, transcription runs first, and every model after it reasons about the words transcription handed over. A wrong word there is a wrong answer three calls later, and the LLM has no way to know it.
The model layer behind that first call has consolidated everywhere except the first call itself. If you build on OpenRouter, you already reach dozens of models through one key, one billing relationship, and one request shape. Speech-to-text sat outside it — a separate vendor, a separate contract, a separate SDK — wrapped around the one component that determines what the rest of your stack sees.
Today, Universal-3.5 Pro is available on OpenRouter, served through our
Sync Speech-to-Text API. It is
listed on OpenRouter’s transcription endpoint as assemblyai/universal-3-5-pro. Pricing
matches our published rates, and OpenRouter passes it through without a markup.
One POST to the endpoint you already call #
One request, one transcript, no job to poll and no WebSocket to hold open:
import base64
import os
import requests
with open("turn.wav", "rb") as f:
audio = base64.b64encode(f.read()).decode("utf-8")
response = requests.post(
"https://openrouter.ai/api/v1/audio/transcriptions",
headers={"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}"},
json={
"model": "assemblyai/universal-3-5-pro",
"input_audio": {"data": audio, "format": "wav"},
},
timeout=30,
)
response.raise_for_status()
print(response.json()["text"])
That is the whole integration. If you are already calling OpenRouter for chat completions, the path and
the model string are the only things that change. The endpoint also accepts OpenAI-style multipart
requests, so an existing OpenAI SDK client pointed at OpenRouter’s base URL works against
Universal-3.5 Pro without a code change. Word timestamps come back through OpenRouter’s own
response_format and timestamp_granularities fields rather than ours, which
keeps the response shape identical to every other transcription model on the endpoint.
Three ways to tell the model what it is about to hear #
Three AssemblyAI options travel in provider.options.assemblyai: prompt,
keyterms_prompt, and conversation_context. They are what change what you get
back on hard audio, and they are the difference between a transcript that is broadly right and one that
gets your customer’s vocabulary right.
prompt describes the situation before the first word arrives. Up to 6,000 characters
telling the model what kind of speech is coming: your domain, your product, the shape of the
conversation. “Transcribe this medical intake conversation” primes the decoder for clinical
vocabulary, which is exactly the ground where a general-purpose model guesses and gets it plausibly
wrong. Plausibly wrong is the expensive failure, because it survives every check downstream of it.
conversation_context passes the preceding turns with each request, up to 500 turns or
16,000 characters, and the model transcribes the current clip against that history. When a caller
answers “yeah, the second one” or reads back a number the agent just asked for, the model
knows what question it is answering. You are only billed for the current turn’s audio, so carrying
the full dialogue costs nothing but request size.
keyterms_prompt takes up to 100 terms per request and biases the model toward strings you
already know: product names, drug names, the SKUs that belong to this one account. Because they are sent
per request, they can be scoped to the individual conversation instead of baked into a global config,
which matters when your vocabulary differs per customer.
curl https://openrouter.ai/api/v1/audio/transcriptions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "assemblyai/universal-3-5-pro",
"input_audio": { "format": "wav", "data": "<base64 16-bit WAV>" },
"response_format": "verbose_json",
"timestamp_granularities": ["word"],
"provider": {
"options": {
"assemblyai": {
"prompt": "Transcribe this medical intake conversation. The audio is in English.",
"keyterms_prompt": ["Trazodone", "Tirzepatide", "Semaglutide"],
"conversation_context": [
"Thanks for calling. How can I help?",
"I need to check on a prescription refill.",
"Sure, which medication?"
]
}
}
}
}'
Built for one turn of speech and an answer right now #
The Sync API is shaped like the request a voice product actually makes: one turn in, one transcript back, in the same response. Called directly, it returns a finished transcript in ~134 ms at p50, against the 5 to 6 seconds of job overhead a submit-and-poll flow adds to audio that takes two seconds to play. A request routed through OpenRouter carries one additional network hop on top of that, so budget accordingly and measure your own p50 from your own region before you wire it into a turn loop.
Clips run from 80 ms to 2 minutes, up to 40 MB, only as WAV. That bound is the design rather than a limit we are working around: audio longer than a turn belongs on the Async API, and a live session that stays open belongs on Realtime. Sync covers the case where the audio is already short and the answer is needed before the user notices waiting.
The accuracy every model after it inherits #
Marketplaces make models look interchangeable, and transcription is the one call in the stack where they are not. Universal-3.5 Pro ranks first for accuracy across independent speech-to-text benchmarks, and it posts a 1.59% normalized word error rate on short-form audio, which is the exact audio profile a turn-shaped request produces.
Proper nouns, alphanumerics, email addresses, and postal addresses are the strings a caller spells out and an agent has to act on, and they are where the gap between transcription models shows up as a failed task rather than a lower score. The model is the same one running behind our Async, Realtime, and Sync surfaces, at the same checkpoint, in all 19 native languages. Routing it through OpenRouter changes the billing relationship and the request shape. It does not change the model.
Key details #
| Model string | assemblyai/universal-3-5-pro | | Endpoint | POST https://openrouter.ai/api/v1/audio/transcriptions | | Request formats | JSON with base64 input_audio, or OpenAI-style multipart | | Auth | Your existing OpenRouter API key | | Clip length | From 80 ms up to 2 minutes of audio per request | | File size | Up to 40 MB | | Input | WAV only, 16 kHz default, auto-resampled | | Languages | 19 native languages, English by default | | AssemblyAI options | prompt, keyterms_prompt, and conversation_context, passed in provider.options.assemblyai | | Timestamps | OpenRouter’s response_format: "verbose_json" with timestamp_granularities: ["word"] | | Latency | ~134 ms p50 measured on our direct Sync endpoint, plus OpenRouter routing | | Price | $0.45/hr of audio, matching our published rates |
Pricing #
$0.45 per hour of audio, billed by duration, matching the Sync API rate on our pricing page. For the first week, pricing will be 50% off of list via OpenRouter.
Get started #
If you have an OpenRouter key, the Python snippet above runs as written. If you want the surfaces OpenRouter doesn’t front, start with a free AssemblyAI account and read the Sync API guide. Both reach the same model.
FAQ #
Is this the same model as AssemblyAI’s own API?
Yes. Same model, same checkpoint, same 19 languages. OpenRouter fronts the Sync API; it does not host a separate copy.
Do I need an AssemblyAI account?
No. Your OpenRouter key is the only credential involved, and billing runs through OpenRouter. You need an AssemblyAI account only for the surfaces OpenRouter doesn’t front.
Does OpenRouter mark up the price?
No. $0.45/hr of audio is our published Sync rate, passed through.
Can I use my existing OpenAI SDK client?
Yes. Point it at OpenRouter’s base URL and set the model string. The endpoint accepts OpenAI-style multipart requests.
How do I get word timestamps?
Set response_format: "verbose_json" and timestamp_granularities: ["word"].
Those are OpenRouter’s fields, not ours.