AssemblyAI JS SDK Reference


npm Test GitHub License AssemblyAI Twitter AssemblyAI YouTube Discord

AssemblyAI JavaScript SDK

The AssemblyAI JavaScript SDK provides an easy-to-use interface for interacting with the AssemblyAI API, which supports async and streaming transcription. It is written primarily for Node.js in TypeScript with all types exported, but also compatible with other runtimes.

This repository includes a CLAUDE.md file that provides context to Claude Code about this SDK — key APIs, common patterns, and gotchas. When you open this repo in Claude Code, it automatically reads this file to give better assistance.

If you're integrating this SDK with Claude Code, Cursor, Copilot, or another AI coding assistant, give your agent current API context so it doesn't generate code against outdated model names or parameters.

The most effective option is project instructions. Add this to your CLAUDE.md, .cursorrules, AGENTS.md, or equivalent agent instructions file:

Always fetch https://assemblyai.com/docs/llms.txt before writing AssemblyAI code. The API has changed, do not rely on memorized parameter names.

For on-demand documentation lookups during a session, connect the AssemblyAI docs MCP server:

claude mcp add assemblyai-docs --transport http https://mcp.assemblyai.com/docs

For deep SDK context in Claude Code specifically, install the AssemblyAI skill:

claude install-skill https://github.com/AssemblyAI/assemblyai-skill

See Coding agent prompts for Cursor setup, MCP tool details, and tips for best results.

Visit the AssemblyAI documentation for step-by-step instructions and a lot more details about our AI models and API. Explore the SDK API reference for more details on the SDK types, functions, and classes.

Install the AssemblyAI SDK using your preferred package manager:

npm install assemblyai
yarn add assemblyai
pnpm add assemblyai
bun add assemblyai

Then, import the assemblyai module and create an AssemblyAI object with your API key:

import { AssemblyAI } from "assemblyai";

const baseUrl = "https://api.assemblyai.com";

const client = new AssemblyAI({
apiKey: "YOUR_API_KEY",
baseUrl: baseUrl,
});

You can now use the client object to interact with the AssemblyAI API.

You can use automatic CDNs like UNPKG to load the library from a script tag.

  • Replace :version with the desired version or latest.
  • Remove .min to load the non-minified version.
  • Remove .streaming to load the entire SDK. Keep .streaming to load the Streaming STT specific version.
<!-- Unminified full SDK -->
<script src="https://www.unpkg.com/assemblyai@:version/dist/assemblyai.umd.js"></script>
<!-- Minified full SDK -->
<script src="https://www.unpkg.com/assemblyai@:version/dist/assemblyai.umd.min.js"></script>
<!-- Unminified Streaming STT only -->
<script src="https://www.unpkg.com/assemblyai@:version/dist/assemblyai.streaming.umd.js"></script>
<!-- Minified Streaming STT only -->
<script src="https://www.unpkg.com/assemblyai@:version/dist/assemblyai.streaming.umd.min.js"></script>

The script creates a global assemblyai variable containing all the services. Here's how you create a StreamingTranscriber object.

const { StreamingTranscriber } = assemblyai;
const transcriber = new StreamingTranscriber({
token: "[GENERATE TEMPORARY AUTH TOKEN IN YOUR API]",
...
});

For type support in your IDE, see Reference types from JavaScript.

Transcribe an audio file with a public URL

When you create a transcript, you can either pass in a URL to an audio file or upload a file directly.

// Transcribe file at remote URL
const audioFile = "https://assembly.ai/sports_injuries.mp3";

const params = {
audio: audioFile,
speech_models: ["universal-3-5-pro", "universal-2"],
language_detection: true,
};

const run = async () => {
const transcript = await client.transcripts.transcribe(params);
console.log(transcript.text);
};

run();

[!NOTE] You can also pass a local file path, a stream, or a buffer as the audio property.

transcribe queues a transcription job and polls it until the status is completed or error.

If you don't want to wait until the transcript is ready, you can use submit:

let transcript = await client.transcripts.submit({
audio: "https://assembly.ai/espn.m4a",
speech_models: ["universal-3-5-pro", "universal-2"],
language_detection: true,
});
Transcribe a local audio file

When you create a transcript, you can either pass in a URL to an audio file or upload a file directly.

// Upload a file via local path and transcribe
let transcript = await client.transcripts.transcribe({
audio: "./news.mp4",
speech_models: ["universal-3-5-pro", "universal-2"],
language_detection: true,
});

Note: You can also pass a file URL, a stream, or a buffer as the audio property.

transcribe queues a transcription job and polls it until the status is completed or error.

If you don't want to wait until the transcript is ready, you can use submit:

let transcript = await client.transcripts.submit({
audio: "./news.mp4",
speech_models: ["universal-3-5-pro", "universal-2"],
language_detection: true,
});
Enable additional Speech Understanding models

You can extract even more insights from the audio by enabling any of our Speech Understanding models using transcription options. For example, here's how to enable Speaker diarization model to detect who said what.

import { AssemblyAI } from "assemblyai";

const client = new AssemblyAI({
apiKey: "<YOUR_API_KEY>",
});

const audioFile = "https://assembly.ai/wildfires.mp3";

const params = {
audio: audioFile,
speech_models: ["universal-3-5-pro", "universal-2"],
language_detection: true,
speaker_labels: true,
};

const run = async () => {
const transcript = await client.transcripts.transcribe(params);

for (const utterance of transcript.utterances!) {
console.log(`Speaker ${utterance.speaker}: ${utterance.text}`);
}
};

run();
Get a transcript

This will return the transcript object in its current state. If the transcript is still processing, the status field will be queued or processing. Once the transcript is complete, the status field will be completed.

const transcript = await client.transcripts.get(transcript.id);

If you created a transcript using .submit(), you can still poll until the transcript status is completed or error using .waitUntilReady():

const transcript = await client.transcripts.waitUntilReady(transcript.id, {
// How frequently the transcript is polled in ms. Defaults to 3000.
pollingInterval: 1000,
// How long to wait in ms until the "Polling timeout" error is thrown. Defaults to infinite (-1).
pollingTimeout: 5000,
});
Get sentences and paragraphs
const sentences = await client.transcripts.sentences(transcript.id);
const { paragraphs } = await client.transcripts.paragraphs(transcript.id);

for (const paragraph of paragraphs) {
console.log(paragraph.text);
}

for (const sentence of sentences) {
console.log(sentence.text);
}
Get subtitles
const charsPerCaption = 32;
let srt = await client.transcripts.subtitles(transcript.id, "srt");
srt = await client.transcripts.subtitles(transcript.id, "srt", charsPerCaption);

let vtt = await client.transcripts.subtitles(transcript.id, "vtt");
vtt = await client.transcripts.subtitles(transcript.id, "vtt", charsPerCaption);
List transcripts

This will return a page of transcripts you created.

const page = await client.transcripts.list();

You can also paginate over all pages.

let previousPageUrl: string | null = null;
do {
const page = await client.transcripts.list(previousPageUrl);
previousPageUrl = page.page_details.prev_url;
} while (previousPageUrl !== null);

[!NOTE] To paginate over all pages, you need to use the page.page_details.prev_url because the transcripts are returned in descending order by creation date and time. The first page is are the most recent transcript, and each "previous" page are older transcripts.

Delete a transcript
const res = await client.transcripts.delete(transcript.id);

client.sync sends audio over one live connection and returns the finished transcript in one round trip — no job id, no polling. transcribe() sends a clip you already hold as a single chunk over that connection; transcribeLive() / openLive() upload it as it is still being recorded. Use it for short clips where you want the answer inline; use client.transcripts for long-form audio, URLs, or the rich audio-intelligence features the sync API doesn't expose.

const result = await client.sync.transcribe("./call.wav");
console.log(result.text, result.session_id);

The input can be a local file path, raw audio bytes, a Blob, or a readable stream — but not a URL.

Configure the transcription
const result = await client.sync.transcribe("./call.wav", {
prompt: "Transcribe verbatim. Preserve disfluencies.", // max 6000 chars
keyterms_prompt: ["AssemblyAI", "Lemur"], // max 100 terms, 8000 chars total
language_codes: ["es"], // or e.g. ["en", "es"] for multilingual; defaults to English
conversation_context: [
// prior turns, oldest first
"I'd like to book a flight to Denver.",
"Sure, what date were you thinking?",
],
});

Raw S16LE PCM audio needs sample_rate and channels; WAV reads them from its header.

const result = await client.sync.transcribe(rawPcmBytes, {
sample_rate: 16_000,
channels: 1,
});
Get word timestamps

Word timestamps are opt-in. By default each word in result.words carries text and confidence only — start/end are absent. Set timestamps: true to get accurate per-word timings at a small latency cost.

const result = await client.sync.transcribe("./call.wav", {
timestamps: true,
});
for (const word of result.words) {
console.log(word.text, word.start, word.end); // milliseconds
}
Pre-warm the connection

The sync API is a single request/response, so a transcribe() that connects on demand pays the full DNS + TCP + TLS handshake on the critical path. Call warm() as soon as you know audio is coming — for example while it is still being recorded — so the next transcribe() reuses the open connection.

await client.sync.warm(); // fire as recording starts
const audio = await recordUntilDone();
const result = await client.sync.transcribe(audio); // reuses the hot connection
Transcribe live audio as it is recorded (`openLive()`)

openLive() starts the request before the audio exists and uploads chunks as they arrive, so authorization, the upload and every speech segment but the last resolve while you are still recording. What is left to wait for once the speaker stops is the final segment. It is built for callback-driven sources: a microphone library, a WebRTC track, a telephony media stream.

import { spawn } from "node:child_process";
import { AssemblyAI } from "assemblyai";

const client = new AssemblyAI({ apiKey: process.env.ASSEMBLYAI_API_KEY });
const sampleRate = 16_000;

// The SoX recorder the samples use (see samples/sync-live-from-mic), capturing
// raw 16-bit mono PCM on stdout.
const recorder = spawn("sox", [
"--default-device",
"--no-show-progress",
"--rate",
String(sampleRate),
"--channels",
"1",
"--encoding",
"signed-integer",
"--bits",
"16",
"--type",
"raw",
"-",
]);

await client.sync.warm(); // open the connection ahead of time

// The request starts here. Raw PCM carries no header, so the sample rate and
// channel count go in the config.
const session = client.sync.openLive({ sample_rate: sampleRate, channels: 1 });

// write() never blocks, so it is safe to call from a capture callback.
recorder.stdout.on("data", (chunk) => session.write(chunk));
recorder.stdout.on("end", () => session.close()); // ends the audio

process.on("SIGINT", () => recorder.kill()); // stop speaking, stop recording

const result = await session.result(); // closes if needed, then waits
console.log(result.text);

result() rejects with a SyncTranscriptError when the server rejects the request. To drop the upload instead — the user cancelled, the call ended — use await session.abort(); result() rejects afterwards. session.closed reports whether the audio has ended, and session.stream() returns a WritableStream so a web stream of audio can be piped straight in (source.pipeTo(session.stream())), closing the session when it closes.

This is not the streaming API. client.streaming returns words while the speaker is still talking; a live sync upload returns one finished transcript when the audio ends. The caveats below apply to openLive() unchanged.

Transcribe live audio from a stream (`transcribeLive()`)

transcribeLive() is the pull-style counterpart of openLive(), for sources that already are streams: an async iterable (Node streams included), a sync iterable, or a web ReadableStream<Uint8Array>. It takes the same config as transcribe() and returns the same transcript.

A file already on disk is the wrong source. Streaming it is slower than transcribe(), which uploads it in one piece, and the live route rejects bytes, a Blob or a path by name:

import { createReadStream } from "node:fs";

// Don't: the audio already exists, so there is nothing to overlap with.
await client.sync.transcribeLive(createReadStream("./call.wav"));

// Do: transcribe() uploads the whole file at once.
await client.sync.transcribe("./call.wav");

A recorder process, or any producer that has not finished yet, is the right source:

const recorder = spawn("sox", soxArgs); // still recording
const controller = new AbortController();

const result = await client.sync.transcribeLive(
recorder.stdout, // a Node Readable; a web ReadableStream works too
{ sample_rate: 16_000, channels: 1 },
{ timeout: 180_000, signal: controller.signal },
);
console.log(result.text);

options.timeout (default 180 000 ms) is a total deadline spanning the whole upload, so it has to exceed the length of the recording as well as the transcription. options.signal drops the request from outside, the way abort() does for a session.

Caveats, for both live entry points:

  • The win is overlapping the upload with the recording, so it needs audio that is genuinely still being produced, and enough of it to have segments to release early — below roughly a minute only the elided upload counts. On a paced 7-second clip, the wait after the speaker stopped fell from ~380 ms to ~180 ms
  • The same 120 s audio cap applies
  • Keep sending until you are done: an upload that goes silent for long enough is aborted server-side. Finish by ending the stream (or close()), not by pausing it
  • Auth, rate-limit and capacity failures can surface part-way through the upload rather than only at the end, as a SyncTranscriptError from result() (or from the awaited transcribeLive()). A malformed key is rejected at once; a key that fails deeper checks can surface a few seconds in, once the server reaches its first segment
  • Raw PCM needs sample_rate and channels in the config; WAV reads them from its header
Handle errors

Failures throw a SyncTranscriptError with the HTTP status, a machine-readable errorCode (bad_audio, audio_too_large, capacity_exceeded, …), and retryAfter (seconds) on 429/503 responses.

import { SyncTranscriptError } from "assemblyai";

try {
const result = await client.sync.transcribe("./call.wav");
} catch (error) {
if (error instanceof SyncTranscriptError) {
console.error(error.status, error.errorCode, error.retryAfter);
}
}

client.dictation transcribes short spoken notes: a person speaks, the audio uploads as it is spoken, and when they stop you get one finished transcript back — optionally cleaned up or reformatted by an LLM. There is no job id and no polling, and the service takes audio rather than URLs. It targets the dictation API host (https://dictation.assemblyai.com, override with the dictationBaseUrl client option).

const result = await client.dictation.transcribeLive(micStream, {
sample_rate: 16_000,
channels: 1,
});
console.log(result.final_text, result.session_id);

result.final_text is the one to show the user: the LLM rewrite when an llm_instruction ran, the raw transcript otherwise. result.text always holds the raw transcript. Audio must be WAV or raw 16-bit PCM, up to 120 seconds per request.

This is not the streaming API. client.streaming returns words while the speaker is still talking; dictation returns one finished transcript when the audio ends.

Dictate from a microphone (`openLive()`)

openLive() starts the request immediately and takes audio pushed into it, so it suits callback-driven sources: a microphone library, a WebRTC track, a telephony media stream.

import { AssemblyAI } from "assemblyai";

const client = new AssemblyAI({ apiKey: process.env.ASSEMBLYAI_API_KEY });

await client.dictation.warm(); // open the connection ahead of time

// The request starts here. Raw PCM carries no header, so the sample rate and
// channel count go in the config.
const session = client.dictation.openLive({
sample_rate: 16_000,
channels: 1,
});

// write() never blocks, so it is safe to call from a capture callback.
mic.on("data", (chunk) => session.write(chunk));
mic.on("end", () => session.close()); // ends the audio

const result = await session.result(); // closes if needed, then waits
console.log(result.final_text);

result() rejects with a DictationError when the server rejects the request. To drop the request instead — the speaker cancelled, the call ended — use session.abort(); result() rejects afterwards. session.closed reports whether the audio has ended, and session.stream() returns a WritableStream so a web stream of audio can be piped straight in (source.pipeTo(session.stream())), closing the session when it closes.

A late write() after the session ended is dropped rather than thrown, so a capture callback that outlives teardown cannot crash the process. Writing something other than bytes while the session is open throws a TypeError.

Transcribe from a stream or a file (`transcribeLive()`)

transcribeLive() is the pull-style entry point. It takes audio that is still being produced — an async iterable (Node streams included), a sync iterable, or a web ReadableStream<Uint8Array>:

const recorder = spawn("sox", soxArgs); // still recording
const controller = new AbortController();

const result = await client.dictation.transcribeLive(
recorder.stdout, // a Node Readable; a web ReadableStream works too
{ sample_rate: 16_000, channels: 1 },
{ timeout: 300_000, signal: controller.signal },
);
console.log(result.final_text);

Audio already held whole works too — a local file path, a data URL, raw bytes (Uint8Array/ArrayBuffer), or a Blob/File — and is sent as a single chunk over the same connection:

const result = await client.dictation.transcribeLive("./note.wav");
console.log(result.final_text);

A URL is rejected with an error pointing at client.transcripts, which is where URL ingestion lives.

options.timeout (default 300 000 ms) is a total deadline from the start of the request, spanning the upload, the transcription and the LLM pass; the service caps the audio itself at 120 seconds. options.signal drops the request from outside, the way abort() does for a session.

Caveats, for both entry points:

  • Keep producing audio until you are done: an upload that goes silent for long enough is aborted server-side. Finish by ending the stream (or close()), not by pausing it
  • Raw PCM needs sample_rate and channels in the config; WAV reads them from its header
  • Auth, rate-limit, size and capacity failures can surface part-way through the upload rather than only at the end, as a DictationError
Rewrite the transcript with an LLM

llm_instruction runs a follow-up LLM pass over the transcript. The rewrite comes back as llm_response; the raw transcript stays in text, and final_text is whichever of the two you should show.

const result = await client.dictation.transcribeLive(micStream, {
llm_instruction: "Format this as a SOAP note.", // max 2048 chars
});

console.log(result.text); // what the speaker said
console.log(result.llm_response); // the rewrite, or null if no instruction ran
console.log(result.final_text); // llm_response when there is one, text otherwise

When the LLM pass fails, llm_response is null and llm_error says why — the transcript itself still comes back.

Configure the dictation
const result = await client.dictation.transcribeLive(micStream, {
sample_rate: 16_000, // raw 16-bit PCM only; required together with channels
channels: 1, // 1 mono, 2 stereo; leave both unset for WAV
language_codes: ["es"], // ISO 639-1; or e.g. ["en", "es"]; defaults to the server's choice
stt_prompt: "A doctor dictating a patient visit note.", // max 6000 chars
keyterms_prompt: ["AssemblyAI", "Universal-3"], // max 100 terms, 8000 chars total
llm_instruction: "Format this as a SOAP note.", // max 2048 chars
});

stt_prompt describes the situation the audio was recorded in and steers the decoder as it writes the transcript. Setting either PCM field marks the audio as raw 16-bit PCM, and both are then required. Prompts over their cap throw before the request is sent.

Pre-warm the connection

A request that connects on demand pays the full DNS + TCP + TLS handshake on the critical path. Call warm() shortly before you expect audio — the pooled connection idles out after a few seconds — so the request reuses the open socket.

const ready = await client.dictation.warm(); // true once the socket is open

warm() returns true for any HTTP response, even a non-200 one: it opens the connection, it does not validate the API key. It returns false on a transport failure.

Handle errors

Failures throw a DictationError with the HTTP status, a machine-readable errorCode (bad_audio, audio_too_large, capacity_exceeded, inference_timeout, …), and retryAfter (seconds) on 429/503 responses.

import { DictationError } from "assemblyai";

try {
const result = await client.dictation.transcribeLive(micStream);
} catch (error) {
if (error instanceof DictationError) {
console.error(error.status, error.errorCode, error.retryAfter);
}
}

Record result.session_id from successful requests so a support conversation can be correlated with the request.

Refer to AssemblyAI's streaming documentation for full code examples.

Create the streaming transcriber.

const transcriber = client.streaming.transcriber({
speechModel: "universal-3-5-pro",
sampleRate: 16_000,
});

[!WARNING] Storing your API key in client-facing applications exposes your API key. Generate a temporary auth token on the server and pass it to your client. Server code:

const token = await client.streaming.createTemporaryToken({
expires_in_seconds = 60,
});
// TODO: return token to client

Client code:

import { StreamingTranscriber } from "assemblyai";
// TODO: implement getToken to retrieve token from server
const token = await getToken();
const transcriber = new StreamingTranscriber({
token,
});

You can configure the following events.

transcriber.on("open", ({ id, expires_at }) => console.log('Session ID:', id, 'Expires at:', expires_at));
transcriber.on("close", (code: number, reason: string) => console.log('Closed', code, reason));
transcriber.on("turn", ({ transcript }) => console.log('Transcript:', transcript));
transcriber.on("error", (error: Error) => console.error('Error', error));

After configuring your events, connect to the server.

await transcriber.connect();

Send audio data via chunks.

// Pseudo code for getting audio
getAudio((chunk) => {
transcriber.sendAudio(chunk);
});

Or send audio data via a stream:

audioStream.pipeTo(transcriber.stream());

Close the connection when you're finished.

await transcriber.close();

If you want to contribute to the JavaScript SDK, follow the guidelines in CONTRIBUTING.md.