AssemblyAI Free code examples in C#, Python, Java and JavaScript
Transcription with summarisation, topics and speaker detection on top.
WHAT THIS PAGE COVERS
Async transcription with a webhook instead of polling.
universal-3-5-pro
assemblyai
Model ids change. If a sample returns a 404 on the model, that is the first thing to check against the official docs.
ASSEMBLYAI DISCONTINUED ITS C# SDK IN APRIL 2025. The published NuGet releases still resolve but are unmaintained, and AssemblyAI's own guidance is to call the API. So this is HttpClient and System.Text.Json — no dependency to go stale.
# No package. HttpClient and System.Text.Json ship with the framework.
dotnet new console -o AssemblyAiDemo
setx ASSEMBLYAI_API_KEY "your-key" # Windows, then open a new shell
export ASSEMBLYAI_API_KEY="your-key" # macOS / Linux
The header is `authorization` with the key alone — no `Bearer` prefix, which is the reflex and returns 401. The response is immediate and carries an id, not a transcript.
using System.Net.Http.Json;
using System.Text.Json;
var apiKey = Environment.GetEnvironmentVariable("ASSEMBLYAI_API_KEY")
?? throw new InvalidOperationException("ASSEMBLYAI_API_KEY is not set.");
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("authorization", apiKey);
var submitted = await http.PostAsJsonAsync(
"https://api.assemblyai.com/v2/transcript",
new
{
audio_url = "https://assembly.ai/wildfires.mp3",
speech_models = new[] { "universal-3-5-pro" },
speaker_labels = true,
});
submitted.EnsureSuccessStatusCode();
var job = await submitted.Content.ReadFromJsonAsync<JsonElement>();
var id = job.GetProperty("id").GetString();
Same call with three more fields. The auth header fields are not optional in practice: without them your endpoint accepts a completed transcript from anyone who guesses the URL.
var withWebhook = await http.PostAsJsonAsync(
"https://api.assemblyai.com/v2/transcript",
new
{
audio_url = "https://assembly.ai/wildfires.mp3",
speech_models = new[] { "universal-3-5-pro" },
webhook_url = "https://example.com/hooks/assemblyai",
webhook_auth_header_name = "X-Webhook-Secret",
webhook_auth_header_value =
Environment.GetEnvironmentVariable("WEBHOOK_SECRET"),
});
withWebhook.EnsureSuccessStatusCode();
The body carries transcript_id and status. Fetch the transcript yourself once the status is completed — and check the header before spending the request.
app.MapPost("/hooks/assemblyai", async (HttpContext context, IHttpClientFactory factory) =>
{
var secret = Environment.GetEnvironmentVariable("WEBHOOK_SECRET");
if (context.Request.Headers["X-Webhook-Secret"] != secret)
{
return Results.Unauthorized();
}
var payload = await context.Request.ReadFromJsonAsync<JsonElement>();
if (payload.GetProperty("status").GetString() != "completed")
{
return Results.NoContent();
}
var id = payload.GetProperty("transcript_id").GetString();
var client = factory.CreateClient("assemblyai");
var transcript = await client.GetFromJsonAsync<JsonElement>(
$"https://api.assemblyai.com/v2/transcript/{id}");
Console.WriteLine(transcript.GetProperty("text").GetString());
return Results.Ok();
});
One package. Omitting the model entirely auto-upgrades you to the latest Universal Pro; pinning it, as below, means your transcripts do not change under you on a Tuesday.
pip install assemblyai
export ASSEMBLYAI_API_KEY="your-key" # macOS / Linux
setx ASSEMBLYAI_API_KEY "your-key" # Windows, then open a new shell
The import path is assemblyai.prerecorded.v2 — the flat `import assemblyai as aai` entry point from older tutorials is gone. transcribe() blocks until the job finishes, which is fine for a script and wrong for a web request.
import os
from assemblyai.prerecorded.v2 import Transcriber
transcriber = Transcriber(api_key=os.environ["ASSEMBLYAI_API_KEY"])
transcript = transcriber.transcribe(
"https://assembly.ai/wildfires.mp3",
speech_models=["universal-3-5-pro"],
)
print(transcript.text)
The prompt is the cheapest accuracy win available: a sentence naming the speakers, the product and the domain measurably improves a noisy recording. Keep it short — it is context, not instructions.
transcript = transcriber.transcribe(
"https://assembly.ai/wildfires.mp3",
speech_models=["universal-3-5-pro"],
speaker_labels=True,
prompt="A wildfire briefing. Speakers are a reporter and a fire chief.",
)
for utterance in transcript.utterances:
print(f"[speaker {utterance.speaker}] {utterance.text}")
THIS IS THE REASON TO USE THE ASYNC API. Polling a long transcription burns a request every few seconds and holds a worker open; a webhook costs nothing until the job is done. The auth header fields are what stop anyone else posting to your endpoint.
import os
import requests
response = requests.post(
"https://api.assemblyai.com/v2/transcript",
headers={"authorization": os.environ["ASSEMBLYAI_API_KEY"]},
json={
"audio_url": "https://assembly.ai/wildfires.mp3",
"speech_models": ["universal-3-5-pro"],
"speaker_labels": True,
"webhook_url": "https://example.com/hooks/assemblyai",
# AssemblyAI sends this header back to you. Check it, or your
# endpoint will accept a transcript from anyone who finds it.
"webhook_auth_header_name": "X-Webhook-Secret",
"webhook_auth_header_value": os.environ["WEBHOOK_SECRET"],
},
timeout=30,
)
print("submitted:", response.json()["id"])
The POST body carries the transcript id and status, NOT the transcript. Fetch it yourself — which is also what makes the auth header check worth doing before you spend the request.
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/hooks/assemblyai")
def on_transcript():
if request.headers.get("X-Webhook-Secret") != os.environ["WEBHOOK_SECRET"]:
abort(401)
payload = request.get_json()
if payload["status"] != "completed":
return "", 204
transcript = requests.get(
f"https://api.assemblyai.com/v2/transcript/{payload['transcript_id']}",
headers={"authorization": os.environ["ASSEMBLYAI_API_KEY"]},
timeout=30,
).json()
print(transcript["text"])
return "", 200
ASSEMBLYAI DISCONTINUED ITS JAVA SDK IN APRIL 2025 — com.assemblyai:assemblyai-java still resolves on Maven Central and is unmaintained. This uses java.net.http, which is in the JDK, plus Jackson for the two fields worth reading.
# Gradle — only for JSON parsing.
implementation("com.fasterxml.jackson.core:jackson-databind:2.18.2")
export ASSEMBLYAI_API_KEY="your-key"
The header is `authorization` with the bare key. speech_models is an array even when you name one model, and the singular speech_model is deprecated for pre-recorded audio.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Submit {
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("ASSEMBLYAI_API_KEY");
if (apiKey == null) throw new IllegalStateException("ASSEMBLYAI_API_KEY is not set.");
String body = "{"
+ "\"audio_url\":\"https://assembly.ai/wildfires.mp3\","
+ "\"speech_models\":[\"universal-3-5-pro\"],"
+ "\"speaker_labels\":true}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.assemblyai.com/v2/transcript"))
.header("authorization", apiKey)
.header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
}
Three extra fields and the submit returns straight away. Nothing polls, and the auth header is what keeps your endpoint from accepting someone else's transcript.
String secret = System.getenv("WEBHOOK_SECRET");
String body = "{"
+ "\"audio_url\":\"https://assembly.ai/wildfires.mp3\","
+ "\"speech_models\":[\"universal-3-5-pro\"],"
+ "\"webhook_url\":\"https://example.com/hooks/assemblyai\","
+ "\"webhook_auth_header_name\":\"X-Webhook-Secret\","
+ "\"webhook_auth_header_value\":\"" + secret + "\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.assemblyai.com/v2/transcript"))
.header("authorization", apiKey)
.header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
The callback gives you an id. This is the GET that turns it into text, and the same call is what a polling loop would make — the difference is that you make it once.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
String transcriptId = new ObjectMapper()
.readTree(response.body())
.path("id")
.asText();
HttpRequest fetch = HttpRequest.newBuilder()
.uri(URI.create("https://api.assemblyai.com/v2/transcript/" + transcriptId))
.header("authorization", apiKey)
.GET()
.build();
HttpResponse<String> finished = HttpClient.newHttpClient()
.send(fetch, HttpResponse.BodyHandlers.ofString());
JsonNode root = new ObjectMapper().readTree(finished.body());
System.out.println(root.path("text").asText());
for (JsonNode utterance : root.path("utterances")) {
System.out.printf("[speaker %s] %s%n",
utterance.path("speaker").asText(), utterance.path("text").asText());
}
The npm package is `assemblyai`, and the client is a named export rather than a default one.
npm install assemblyai
export ASSEMBLYAI_API_KEY="your-key"
transcribe() submits and waits. For anything a user is sitting in front of, use the webhook path further down instead — a long recording will outlive the request.
import { AssemblyAI } from "assemblyai";
const client = new AssemblyAI({ apiKey: process.env.ASSEMBLYAI_API_KEY });
const transcript = await client.transcripts.transcribe({
audio: "https://assembly.ai/wildfires.mp3",
speech_models: ["universal-3-5-pro"],
});
console.log(transcript.text);
speaker_labels groups the text into utterances. Read transcript.utterances rather than reassembling it from words — the grouping is the work you would otherwise redo badly.
const transcript = await client.transcripts.transcribe({
audio: "https://assembly.ai/wildfires.mp3",
speech_models: ["universal-3-5-pro"],
speaker_labels: true,
prompt: "A wildfire briefing. Speakers are a reporter and a fire chief.",
});
for (const utterance of transcript.utterances ?? []) {
console.log(`[speaker ${utterance.speaker}] ${utterance.text}`);
}
Submitting with a webhook_url returns immediately with an id. Nothing polls, nothing holds a connection open, and a forty-minute recording costs one request instead of four hundred.
const submitted = await fetch("https://api.assemblyai.com/v2/transcript", {
method: "POST",
headers: {
authorization: process.env.ASSEMBLYAI_API_KEY,
"content-type": "application/json",
},
body: JSON.stringify({
audio_url: "https://assembly.ai/wildfires.mp3",
speech_models: ["universal-3-5-pro"],
webhook_url: "https://example.com/hooks/assemblyai",
webhook_auth_header_name: "X-Webhook-Secret",
webhook_auth_header_value: process.env.WEBHOOK_SECRET,
}),
});
const { id } = await submitted.json();
console.log("submitted:", id);
The callback body has the id and the status, not the text. Verify the header first — an unauthenticated transcript endpoint is a free write into whatever you do with the result.
import express from "express";
const app = express();
app.use(express.json());
app.post("/hooks/assemblyai", async (request, response) => {
if (request.get("X-Webhook-Secret") !== process.env.WEBHOOK_SECRET) {
return response.sendStatus(401);
}
const { transcript_id: id, status } = request.body;
if (status !== "completed") return response.sendStatus(204);
const result = await fetch(
`https://api.assemblyai.com/v2/transcript/${id}`,
{ headers: { authorization: process.env.ASSEMBLYAI_API_KEY } },
).then((r) => r.json());
console.log(result.text);
response.sendStatus(200);
});
app.listen(3000);
QUESTIONS
Which languages are the AssemblyAI examples in?
C#, Python, Java and JavaScript. Every sample runs as pasted and reads its credentials from an environment variable — no key is ever written into the code.
Which package do I install for AssemblyAI?
The samples use assemblyai. The install step on each language tab gives the exact command.
Which AssemblyAI model do these examples use?
universal-3-5-pro, checked against the official documentation on 26 September 2026. Model ids change often; if a call returns a model-not-found error, check the current list in the official docs.
Where is the official AssemblyAI documentation?
https://www.assemblyai.com/docs — it is linked at the bottom of this page, and it is the authority whenever it disagrees with a sample here.
ALSO IN SPEECH & AUDIO
The authority
Official AssemblyAI documentation
Model names, pricing and rate limits change faster than any page here can. When this page and the official docs disagree, the official docs are right.
https://www.assemblyai.com/docs