SKILL.md
Full skill instructions
AssemblyAI Speech-to-Text and Voice AI
AssemblyAI provides speech-to-text APIs, audio intelligence models, and an LLM Gateway for applying language models to transcripts. This skill corrects common mistakes that training data gets wrong — deprecated APIs, discontinued SDKs, and non-obvious auth patterns.
Authentication
All endpoints use the same header:
Authorization: YOUR_API_KEY
NOT Authorization: Bearer ... — just the raw API key, no Bearer prefix. This is the #1 mistake.
Base URLs
| Service | US | EU |
|---|---|---|
| REST API (async) | https://api.assemblyai.com | https://api.eu.assemblyai.com |
| Sync STT API (≤120s) | https://sync.assemblyai.com (global default, routes to nearest; https://sync.us.assemblyai.com for US residency) | https://sync.eu.assemblyai.com |
| LLM Gateway | https://llm-gateway.assemblyai.com/v1 | https://llm-gateway.eu.assemblyai.com/v1 |
| Streaming v3 | wss://streaming.assemblyai.com/v3/ws | wss://streaming.eu.assemblyai.com/v3/ws |
| Streaming v2 (legacy) | wss://api.assemblyai.com/v2/realtime/ws | — |
| Voice Agent API | wss://agents.assemblyai.com/v1/ws | wss://agents.eu.assemblyai.com/v1/ws |
Streaming EU region: As of March 2026, the EU region moved from AWS eu-west-1 (Ireland) to AWS eu-north-1 (Stockholm). The customer-facing endpoint host (streaming.eu.assemblyai.com) is unchanged.
SDKs
| Language | Package | Status |
|---|---|---|
| Python | pip install assemblyai | Active |
| JavaScript/TypeScript | npm i assemblyai | Active |
| Ruby | assemblyai gem | Active |
| Java | assemblyai-java-sdk | Discontinued April 2025 |
| Go | assemblyai-go-sdk | Discontinued April 2025 |
| C# .NET | AssemblyAI NuGet | Discontinued April 2025 |
Only Python, JS/TS, and Ruby SDKs are maintained. For Java, Go, or C#, use the REST API directly.
Speech-to-Text Models
Pre-Recorded
| Model | Languages | Best For |
|---|---|---|
| Universal-3.5 Pro | 18 (auto-falls back to Universal-2 for the other 99) | Latest flagship: best accuracy, native code-switching across its 18 languages, contextual prompt, keyterms up to 1,000 words |
| Universal-3 Pro | 6 (en, es, de, fr, pt, it) | Highest accuracy, promptable transcription, keyterms up to 1,000 words |
| Universal-2 | 99 | Broadest language coverage, keyterms up to 200 words |
Use speech_models as a priority list with fallback: ["universal-3-pro", "universal-2"] (the default when omitted).
Universal-3.5 Pro is also available for pre-recorded/async transcription, not just streaming. Opt in with speech_models: ["universal-3-5-pro"] on POST /v2/transcript. It supports contextual prompt (a plain-language description of the audio — domain/scenario/full detail) and keyterms_prompt (up to 1,000 terms), does native code-switching, and auto-falls back to Universal-2 for languages outside its 18. Note: the formal OpenAPI speech_models enum still lists only universal-3-pro/universal-2, but the async API accepts universal-3-5-pro.
Streaming
| Model | Languages | Best For |
|---|---|---|
| universal-3-5-pro | 18 | Recommended default for new realtime/streaming code — next-gen flagship: more languages, improved prompting + conversational context |
| u3-rt-pro | 6 | Universal-3 Pro Streaming — punctuation-based turn detection, promptable, mode accuracy/latency tradeoff |
| universal-streaming-english | 1 (English) | Voice agents, ~300ms latency |
| universal-streaming-multilingual | 6 | Per-utterance language detection |
| whisper-rt | 99+ | Legacy — removed from the public model picker (June 2026) and the streaming spec enums, but still functional via speech_model: whisper-rt for broadest streaming language coverage, auto-detect only |
For realtime/streaming STT, use speech_model: "universal-3-5-pro" by default. The raw API parameter is optional and defaults to universal-3-5-pro; set it explicitly when pinning behavior or using an SDK that requires the field. The mode connection param (universal-3-5-pro / u3-rt-pro) trades off accuracy vs latency: min_latency, balanced (default), max_accuracy.
Medical Mode (Add-On)
domain: "medical-v1" enables Medical Mode — an add-on that improves accuracy for medical terminology (medications, procedures, conditions, dosages). Works with both pre-recorded and streaming models.
- Pre-recorded: Universal-3 Pro (
domain: "medical-v1"in request body), Universal-2 - Streaming: universal-3-5-pro, u3-rt-pro, universal-streaming-english, universal-streaming-multilingual
- Supported languages: English, Spanish, German, French (4 languages only)
- Billed as a separate add-on. If used with an unsupported language, the API ignores
domainand returns a warning — transcript still completes and you are NOT charged for Medical Mode.
Prompting (Universal-3.5 Pro)
prompt and keyterms_prompt are complementary — use either, or both together. Neither changes the output format, and both work the same way for streaming and async (POST /v2/transcript). Transcription behavior (verbatim, punctuation, formatting) is built in and managed by AssemblyAI.
prompt(string, ~1500 chars max): a plain-language description of the audio — its domain, scenario, or full details. It carries context, not instructions — formatting/behavioral commands (punctuation rules, "transcribe verbatim", "don't…") are not supported and are ignored. The model stays grounded in the audio: irrelevant or only-partially-applicable context won't make it insert words that weren't spoken, so you can safely send the same description on every session/segment.keyterms_prompt(string[]): an explicit list of names/brands/domain terms to boost. Streaming: up to 100 terms, ≤50 chars each. Async: up to 1,000 terms.
Contextual prompt — three levels of specificity (use the least specific that covers your case):
| Level | Length | Contains | Example |
|---|---|---|---|
| Domain | 2–5 words | The field only | Medical consultation call. |
| Scenario | 5–15 words | What the call is about | Cardiology consultation about chest pain symptoms. |
| Detailed | 20–50 words | Names, products, identifiers | Cardiology consultation between Dr. Smith and a patient about recurring chest pain, ECG results, and hypertension medication. |
Best practices:
- Start with no
promptand nokeyterms_prompt— the model is optimized out of the box. Add context only for domain vocabulary it gets wrong, starting at the broadest level. - Write plain, complete sentences that describe the recording; keep it to one short block, not a keyword list (that's what
keyterms_promptis for). - Keyterms: use exact spelling/capitalization; avoid common words (over-inclusion causes overcorrection/hallucination).
- Specify language via
language_code(preferred) or by naming it in the prompt (e.g. "Spanish customer support call…"). - Streaming: update both mid-session via
UpdateConfiguration; a new keyterms array replaces the prior set,[]clears it.
Sync STT API (short-form audio, ≤120s)
A separate synchronous endpoint for short clips — send audio in one HTTP request, get the transcript back in the response. No polling, no transcript ID, no upload step. Ideal for voice-message transcription, short call recordings, or voice-agent pipelines that do their own turn detection and submit completed utterances.
- Endpoint:
POST https://sync.assemblyai.com/transcribe(global default — routes to nearest region; usesync.us.assemblyai.com/sync.eu.assemblyai.comfor data residency) - Required header:
X-AAI-Model— the current quickstart usesuniversal-3-5-pro;u3-sync-pro(Universal-3 Pro) is also accepted (the value in the formal spec enum) - Auth:
Authorization: YOUR_API_KEY(Bearer prefix optional here, unlike the async REST API; or pass?token=YOUR_API_KEY) - Body:
multipart/form-datawith anaudiopart (Content-Type: audio/wavoraudio/pcm) and an optionalconfigJSON part configfields:prompt(≤4096 chars),word_boost(string[], ≤2048 chars total — this is the documented keyterms param for Sync, notkeyterms_prompt),conversation_context(string or string[] — prior conversation turns oldest-first, for continuity across a multi-turn conversation; oldest dropped when over the context budget),language_code(ISO 639-1 string or list, defaulten— steers the default prompt toward the named language(s); ignored when a custompromptis set), and foraudio/pcmalsosample_rate+channels(required for raw PCM; WAV reads them from its header)- Audio limits: 80ms–120s, ≤40MB, 16-bit only, mono/stereo (stereo down-mixed), sample rates 8000/16000/22050/24000/32000/44100/48000 Hz
- Response:
{ text, words[{text, start, end, confidence}], confidence, audio_duration_ms, session_id }— word timestamps usestart/end(integer milliseconds), same field names as the async API; only the clip-levelaudio_duration_mscarries the_mssuffix - 30s per-request deadline (504
inference_timeout). For audio >120s use the async REST API; for live mic audio use Streaming.
curl -X POST https://sync.assemblyai.com/transcribe \
-H 'Authorization: YOUR_API_KEY' \
-H 'X-AAI-Model: universal-3-5-pro' \
-F 'audio=@sample.wav;type=audio/wav'
LeMUR is Deprecated
LeMUR is deprecated (sunset March 31, 2026 — already sunset). Use the LLM Gateway instead. The LLM Gateway is an OpenAI-compatible API. Key difference: you pass transcript text directly in messages (no transcript_ids). Transcribe first, then include transcript.text in your prompt.
See references/llm-gateway.md for models, tool calling, structured outputs, and examples.
Key Gotchas
| Gotcha | Details |
|---|---|
prompt + keyterms_prompt | Complementary for Universal-3.5 Pro — use either or both together. prompt is a contextual description of the audio; keyterms_prompt is an explicit term list. Neither changes output formatting |
summarization / auto_chapters | Deprecated. Use LLM Gateway instead (transcribe → send text to LLM) |
| PII redaction scope | Only redacts words in text — other feature outputs (entities, summaries) may still expose sensitive data |
| Upload key scoping | Files uploaded with one API key project cannot be transcribed with a different project's key |
| Structured outputs | Supported by OpenAI, Gemini, Claude 4.5+, Qwen, and Kimi — Claude 3.x does NOT support json_schema structured outputs |
| U3 Pro-family turn detection | universal-3-5-pro and u3-rt-pro use punctuation (. ? !), NOT confidence thresholds — end_of_turn_confidence_threshold has no effect |
prompt is context, not instructions | Universal-3.5 Pro's prompt describes the audio (domain/scenario/details). Formatting or behavioral commands (punctuation rules, "transcribe verbatim", negative directives like "don't…") are not supported and are ignored — transcription behavior is managed internally |
| PII audio redaction method | override_audio_redaction_method: "silence" replaces PII with silence instead of default beep |
| Language detection | Requires minimum 15 seconds of spoken audio for reliable results |
| LLM Gateway EU region | Only Anthropic Claude and Google Gemini models available — OpenAI models are NOT supported in EU |
| Disfluencies | Enable with disfluencies: true to keep "um"/"uh" in the transcript |
| Medical Mode unsupported language | API silently skips Medical Mode and does not charge for it — check for warning in response |
| Voice Agent API URL | The Voice Agent endpoint is wss://agents.assemblyai.com/v1/ws — NOT /v1/voice (renamed April 2026), /v1/realtime (older), or speech-to-speech.us.assemblyai.com (very old) |
Voice Agent tool.call field | The argument dict is named arguments, not args (renamed April 2026) |
Voice Agent stored agents (agent_id) | The first session.update either binds a reusable stored agent via {"agent_id":"<id>"} (the only field in session) OR sends inline config (system_prompt/greeting/tools/input/output) — the two modes are mutually exclusive; sending both raises a validation error. Create stored agents with POST https://agents.assemblyai.com/v1/agents |
| Voice Agent turn detection fields | Use min_silence (default 1000ms) and max_silence (default 3000ms) under session.input.turn_detection — min_turn_silence/max_turn_silence are the streaming/LiveKit/Pipecat field names, not Voice Agent API. Both must be in [50, 10000] ms with min_silence < max_silence. Setting either explicitly disables adaptive endpointing for the rest of the session |
| Voice Agent immutable fields | After session.ready, immutable: greeting, output.voice, output.format — changing them returns immutable_field. Mutable: system_prompt, input.turn_detection, input.keyterms (up to 100 strings), output.volume (0–100), tools, input.format |
| Voice Agent greeting | The greeting is sent straight to the TTS engine — it is NOT passed through the LLM. Whatever string you set is exactly what the user hears, word for word. Don't write meta-greetings like "Greet the user warmly" — TTS will literally speak that |
| Voice Agent hold-mode transcripts | While an execution_mode: "hold" tool is in flight, transcript.user.delta / transcript.user are NOT emitted in real time — they flush when the hold ends (on tool.result or reply.create) |
| Voice Agent audio pacing | Don't stream audio faster than realtime — excess frames are dropped server-side |
| Voice Agent session teardown billing | Just closing the WebSocket holds the session for 30s (resumable via session.resume) and that grace window is billable. Send session.end ({"type":"session.end"}) when the call is over to close immediately and stop billing — the server replies with a final session.ended (carrying session_duration_seconds, audio_duration_seconds) before closing the socket |
Streaming format_turns digit rendering | format_turns=true enables punctuation, casing, and inverse text normalization (dates, times, phone numbers) — it does NOT control digit rendering. Numerals like "22" are a model behavior, and lexical number output ("twenty-two") is not supported in streaming |
| Streaming EU region | Moved from Ireland (eu-west-1) to Stockholm (eu-north-1) in March 2026. Endpoint host (streaming.eu.assemblyai.com) is unchanged |
LLM Gateway tool_calls location | tool_calls lives at choices[i].message.tool_calls (under message), NOT at choices[i].tool_calls (under choice). content is null when only tool_calls are present |
LLM Gateway finish_reason is provider-native | Don't branch tool-calling loops on finish_reason == "tool_calls" — the Gateway passes the provider's value through, so Claude returns tool_use/end_turn (OpenAI returns tool_calls/stop). Detect a tool call by the presence of message.tool_calls, not by finish_reason |
Transcript metadata.warnings | The Transcript response now includes an optional metadata object. When present, metadata.warnings is an array of {message} objects describing issues processed during transcription (e.g. Medical Mode skipped due to unsupported language). metadata is omitted entirely when there is nothing to report |
| U3 Pro-family streaming context carryover | On by default — the model carries prior finalized turns forward as context (per-session, ~3 entries, ~1500 chars). Pass your agent's spoken reply via agent_context (connection-time query param to seed an opening greeting, or mid-stream via UpdateConfiguration) so the model knows the question the user is answering. Use with universal-3-5-pro or u3-rt-pro |
| Streaming diarization revised labels | With speaker_labels enabled, a single SpeakerRevision message is emitted right before Termination (after you send Terminate), containing a revisions array of only the turns whose speaker labels changed (matched by turn_order). Text and word timestamps never change — only speaker assignments. Adds ~400ms latency at session close. Use it for the final, highest-quality attribution |
LLM Gateway model_region: "global" | Optional request field (only accepted value "global") routes to the provider's global, non-region endpoints for lower cost. Live for Anthropic Claude now; Google Gemini 3 series coming soon. Omit for default in-region processing. Effective July 1, 2026, in-region LLM Gateway requests cost 10% more (provider pass-through, no AssemblyAI upcharge) — global routing keeps current pricing |
Common Mistakes
| Mistake | Correction |
|---|---|
Authorization: Bearer KEY | Authorization: KEY (no Bearer prefix) — BUT the Voice Agent API (agents.assemblyai.com) uses Authorization: Bearer KEY |
| Using LeMUR API | Deprecated. Use LLM Gateway instead |
Using summarization or auto_chapters | Deprecated. Use LLM Gateway instead (transcribe then summarize via LLM) |
LeMUR transcript_ids with LLM Gateway | Pass transcript text in messages, not IDs |
anthropic/claude-... model IDs | No provider prefix: claude-sonnet-4-5-20250929 not anthropic/claude-sonnet-4-5-20250929 |
claude-opus-4-20250514 / claude-sonnet-4-20250514 on LLM Gateway | Removed June 2026. Use Claude Opus 4.5/4.6/4.7 or Claude Sonnet 4.5/4.6 |
Uploading to /v2/upload with -d/--data or a JSON body | Use --data-binary @file (raw bytes). -d/JSON returns a valid upload_url but transcription later fails with Transcoding failed. File type application/json |
| Using Java/Go/C# SDKs | Discontinued. Use Python, JS/TS, Ruby, or raw API |
word_boost on the async REST API | Use keyterms_prompt instead. Exception: the Sync STT API does use word_boost (in its config part) — that's its documented keyterms param |
| Hardcoding v2 streaming URL | v3 (/v3/ws) is current; v2 still works but is legacy |
Assuming streaming defaults to u3-rt-pro | Streaming speech_model defaults to universal-3-5-pro at the raw API layer. Set a different model only when you intentionally need legacy behavior, cost tradeoffs, or broader language coverage |
Python SDK rejects universal-3-5-pro | Upgrade to assemblyai>=0.64.21 for Streaming v3 SDK support. Older SDKs such as 0.64.4 validate speech_model against an enum that omits universal-3-5-pro |
aai.SpeechModel.universal_3_pro in Python SDK | Use raw strings: "universal-3-pro", "universal-2" — these enum aliases don't exist in the SDK |
S2S session.update without "session" key | Must wrap config: {"type":"session.update","session":{...}} |
S2S tool schema using {"function":{...}} nesting | S2S tools are flat: {"type":"function","name":"...","description":"...","parameters":{...}} |
| Voice Agent S2S URL | Correct URL: wss://agents.assemblyai.com/v1/ws — not /v1/voice (renamed April 2026), /v1/realtime (older), or speech-to-speech.us.assemblyai.com (very old) |
Voice Agent tool.call args field | Renamed to arguments — event["arguments"] is the parameter dict |
Medical Mode domain: "medical" | Correct value is domain: "medical-v1" |
LLM Gateway tool result role: "function_call_output" | Correct role is "tool" — use {"role": "tool", "tool_call_id": "...", "content": "..."} |
LLM Gateway response choices[i].tool_calls | Tool calls live under message: choices[i].message.tool_calls, not at the choice level |
Sending tool.result immediately on tool.call | Wait until reply.done is the latest event received — sending earlier (mid transition phrase) or later (after a new turn started) breaks turn-taking |
Speech Understanding without the request wrapper | Features nest under speech_understanding.request.<feature> — speech_understanding.translation (no .request) is invalid. Results come back under speech_understanding.response.<feature> |
| Custom Formatting params as booleans | date/phone_number/email are format-pattern strings (e.g. "mm/dd/yyyy"), not true/false. Only format_utterances is a boolean |
Reference Files
Read the relevant reference file based on what the user needs:
| File | When to read |
|---|---|
references/python-sdk.md | Python SDK patterns and examples |
references/js-sdk.md | JavaScript/TypeScript SDK patterns |
references/streaming.md | Real-time/streaming STT, v3 protocol, temp tokens, error codes |
references/voice-agents.md | Voice agent integrations: LiveKit, Pipecat, turn detection, latency optimization |
references/llm-gateway.md | Applying LLMs to transcripts, tool calling, available models |
references/speech-understanding.md | Translation, speaker identification, custom formatting |
references/audio-intelligence.md | PII redaction, diarization, summarization, sentiment, chapters |
references/api-reference.md | Full parameter list, export endpoints, webhooks, upload, PII policies, Sync STT API, Voice Agents REST API (stored agents) |
