> ## Documentation Index
> Fetch the complete documentation index at: https://assemblyai.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Sync STT on OpenRouter

export const ModelBadges = ({models}) => {
  return <div className="flex flex-wrap gap-2 -mt-3 mb-3 not-prose">
      {models.map(model => <span key={model} className="inline-flex items-center rounded-full bg-green-500/15 px-2.5 py-0.5 text-xs font-mono text-green-700 dark:text-green-400 ring-1 ring-inset ring-green-500/30">
          {model}
        </span>)}
    </div>;
};

## Overview

<ModelBadges models={["universal-3-5-pro"]} />

[OpenRouter](https://openrouter.ai/) is a model gateway: one API key and one bill across many providers, behind an OpenAI-compatible interface. Selecting the model `assemblyai/universal-3-5-pro` on its [transcription endpoint](https://openrouter.ai/docs/guides/overview/multimodal/stt) routes the request to AssemblyAI's [Sync API](/docs/sync-stt/getting-started/quickstart).

You send OpenAI-shaped fields; AssemblyAI's own options travel in a `provider.options.assemblyai` block that OpenRouter forwards untouched.

### Routing through OpenRouter, or calling us directly

**Route through OpenRouter** when you already buy inference through it and want transcription on the same key, bill, and client — or when you're comparing speech models and want to swap providers by changing one string.

**Call the Sync API directly** when you want the things a gateway can't pass through: [live upload](/docs/sync-stt/getting-started/quickstart), which streams audio while the person is still speaking so you only wait on the final stretch; [connection pre-warming](/docs/sync-stt/connection-pre-warming); [data residency](/docs/sync-stt/endpoints-and-data-zones) endpoint selection; and our [SDKs](/docs/sync-stt/getting-started/transcribe-a-short-audio-file).

<CardGroup cols={2}>
  <Card title="OpenRouter STT docs" icon="route" href="https://openrouter.ai/docs/guides/overview/multimodal/stt">
    OpenRouter's reference for the transcription endpoint.
  </Card>

  <Card title="Sync STT quickstart" icon="bolt" href="/docs/sync-stt/getting-started/quickstart">
    Use the Sync API directly, without the gateway.
  </Card>
</CardGroup>

## Quickstart

Set your OpenRouter key:

```bash theme={null}
export OPENROUTER_API_KEY=<your-key>
```

Then POST base64-encoded audio. This example transcribes a medical intake call with a prompt, keyterms, and the preceding turns of the conversation:

<Tabs groupId="language">
  <Tab title="cURL" language="curl" default>
    ```bash theme={null}
    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.",
              "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?"
              ]
            }
          }
        }
      }'
    ```
  </Tab>

  <Tab title="Python" language="python">
    ```python theme={null}
    import base64, os, requests

    with open("intake.wav", "rb") as f:
        audio = base64.b64encode(f.read()).decode()

    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": {"format": "wav", "data": audio},
            "response_format": "verbose_json",
            "timestamp_granularities": ["word"],
            "provider": {
                "options": {
                    "assemblyai": {
                        "prompt": "Transcribe this medical intake conversation.",
                        "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?",
                        ],
                    }
                }
            },
        },
    )

    result = response.json()
    print(result["text"])
    for word in result["words"]:
        print(f"{word['start']} - {word['end']}  {word['word']}")
    ```
  </Tab>
</Tabs>

<Note>
  OpenRouter also accepts an OpenAI-style `multipart/form-data` upload with a `file` part, which makes the basic case a drop-in for the OpenAI SDK. AssemblyAI's options are a nested object, so reach for the JSON body above whenever you need `provider.options`.
</Note>

## Parameter mapping

OpenAI-compatible fields sit at the top level of the request:

| OpenRouter field          | Sync API equivalent                                     | Notes                                            |
| ------------------------- | ------------------------------------------------------- | ------------------------------------------------ |
| `model`                   | `X-AAI-Model` header                                    | `assemblyai/universal-3-5-pro`                   |
| `input_audio.data`        | the `audio` part                                        | Base64 of the raw file bytes                     |
| `input_audio.format`      | the part's content type                                 | Only `wav` — see [Audio](#audio)                 |
| `language`                | [`config.language_codes`](/docs/sync-stt/language-selection) | ISO 639-1                                        |
| `timestamp_granularities` | [`config.timestamps`](/docs/sync-stt/word-timestamps)        | `["word"]` needs `response_format: verbose_json` |

<Warning>
  `language` is ignored when a custom `prompt` is set — state the language in your prompt if you need both (see [Specifying the language](/docs/sync-stt/prompting-and-keyterms#specifying-the-language)).
</Warning>

### AssemblyAI options

Everything specific to AssemblyAI goes under `provider.options.assemblyai`, keyed by our provider slug. Each maps to the `config` field of the same name on [`POST /v1/transcribe`](/docs/api-reference/sync-api/transcribe).

<ParamField path="prompt" type="string">
  A natural-language description of what the audio is about — the domain, the
  scenario, the participants. Write it as a description of the audio, not as
  instructions to the model. Maximum **6000 characters**. A managed default is
  applied when you omit it, so test without one first. See
  [Contextual prompting](/docs/sync-stt/prompting-and-keyterms#contextual-prompting).
</ParamField>

<ParamField path="keyterms_prompt" type="string[]">
  Terms to bias the decoder toward — drug names, product names, proper nouns.
  Maximum **100 terms** and **8000 characters** across all of them. See
  [Keyterms prompting](/docs/sync-stt/prompting-and-keyterms#keyterms-prompting).
</ParamField>

<ParamField path="conversation_context" type="string | string[]">
  Prior turns of the same conversation in chronological order, oldest first.
  Gives the model continuity across a multi-turn exchange. Maximum **500 turns**
  and **16000 characters**; context over either cap is trimmed rather than
  rejected, oldest turns first. See
  [Conversation context](/docs/sync-stt/conversation-context).
</ParamField>

## Response

The default `response_format` is `json` — `text` plus a `usage` block, nothing else. `verbose_json` adds `task`, `duration`, `confidence`, and `segments`; `words` arrives on top of those when you also pass `timestamp_granularities: ["word"]`.

```json theme={null}
{
  "text": "I need to check on a prescription refill for Tirzepatide.",
  "usage": { "seconds": 3.119, "cost": 0.000433194791 },
  "task": "transcribe",
  "duration": 3.119,
  "confidence": 0.9941581597006228,
  "segments": [
    { "id": 0, "start": 0, "end": 3.119, "text": "I need to check on a prescription refill for Tirzepatide." }
  ],
  "words": [
    { "word": "I", "start": 0.032, "end": 0.049, "confidence": 0.9976914893043927 },
    { "word": "need", "start": 0.114, "end": 0.361, "confidence": 0.99996769532457 },
    { "word": "to", "start": 0.361, "end": 0.394, "confidence": 0.9999934435319415 },
    { "word": "check", "start": 0.508, "end": 0.705, "confidence": 0.9999849798550975 },
    { "word": "on", "start": 0.771, "end": 0.87, "confidence": 0.9999413524701386 },
    { "word": "a", "start": 0.935, "end": 0.952, "confidence": 0.9993512074196143 },
    { "word": "prescription", "start": 1.017, "end": 1.625, "confidence": 0.9999634040871428 },
    { "word": "refill", "start": 1.674, "end": 2.019, "confidence": 0.9999704369697439 },
    { "word": "for", "start": 2.084, "end": 2.199, "confidence": 0.9999564905017542 },
    { "word": "Tirzepatide.", "start": 2.249, "end": 3.037, "confidence": 0.944761097541834 }
  ]
}
```

A few things to expect from that payload:

* **`words` needs both flags.** `timestamp_granularities: ["word"]` on its own does nothing — with `response_format: "json"` you get back `text` and `usage` and no more.
* **There is no `language` field**, even when you send `language` on the request.
* **`usage` is OpenRouter's accounting**, not ours. These requests bill through your OpenRouter account, and `cost` is in US dollars.

## Limits

* **Audio duration** — [80 ms to 120 s](/docs/sync-stt/audio-requirements). This is the Sync API's limit; OpenRouter adds none of its own.
* **Processing timeout** — 60 s at the gateway.
* **Request size** — 25 MB for a multipart upload. Duration binds long before size does: 120 seconds of 16 kHz mono WAV is under 4 MB, roughly 5 MB once base64-encoded.

### Audio

Send **16-bit WAV**, mono or stereo, at 8000, 16000, 22050, 24000, 32000, 44100, or 48000 Hz. Full constraints are in [Audio requirements](/docs/sync-stt/audio-requirements).

`wav` is the only value of `input_audio.format` this model accepts. The other containers OpenRouter's endpoint supports across its catalog — `mp3`, `flac`, `m4a`, and the rest — are rejected at the gateway before reaching us:

```json theme={null}
{
  "error": {
    "message": "Unsupported input_audio.format \"mp3\" for AssemblyAI; only wav is supported",
    "code": 400
  }
}
```

Transcode to WAV before sending.

## Related

<CardGroup cols={2}>
  <Card title="Sync STT quickstart" icon="bolt" href="/docs/sync-stt/getting-started/quickstart">
    Call the Sync API directly, with live upload.
  </Card>

  <Card title="Prompting and keyterms" icon="bullseye" href="/docs/sync-stt/prompting-and-keyterms">
    Write prompts and keyterm lists that improve accuracy.
  </Card>

  <Card title="Conversation context" icon="comments" href="/docs/sync-stt/conversation-context">
    Carry prior turns into each request.
  </Card>

  <Card title="Audio requirements" icon="waveform-lines" href="/docs/sync-stt/audio-requirements">
    Duration, size, format, and sample-rate constraints.
  </Card>
</CardGroup>
