AssemblyAI Webhooks & Events
Overview
Handle AssemblyAI webhooks for transcription completion. When you submit a transcript with webhook_url, AssemblyAI sends a POST request to your URL when the transcript is completed or fails. One webhook per transcript — no complex event routing needed.
Prerequisites
- HTTPS endpoint accessible from the internet
assemblyai package installed
- API key configured
How AssemblyAI Webhooks Work
- You submit a transcription with
webhook_url parameter
- AssemblyAI processes the audio asynchronously
- When done (completed or error), AssemblyAI sends a POST to your URL
- Your endpoint receives transcript ID and status, then fetches the full transcript
Key difference from other APIs: AssemblyAI webhooks are per-transcript (set at submission time), not a global webhook registration. There are no event types to subscribe to — you get one callback per transcript.
Instructions
Step 1: Submit Transcription with Webhook
import { AssemblyAI } from 'assemblyai';
const client = new AssemblyAI({
apiKey: process.env.ASSEMBLYAI_API_KEY!,
});
const transcript = await client.transcripts.submit({
audio: 'https://example.com/meeting-recording.mp3',
webhook_url: 'https://your-app.com/webhooks/assemblyai',
webhook_auth_header_name: 'X-Webhook-Secret',
webhook_auth_header_value: process.env.ASSEMBLYAI_WEBHOOK_SECRET!,
speaker_labels: true,
sentiment_analysis: true,
auto_highlights: true,
});
console.log('Submitted:', transcript.id);
Step 2: Webhook Endpoint (Express.js)
import express from 'express';
import { AssemblyAI, type Transcript } from 'assemblyai';
const app = express();
const client = new AssemblyAI({
apiKey: process.env.ASSEMBLYAI_API_KEY!,
});
app.post('/webhooks/assemblyai', express.json(), async (req, res) => {
const secret = req.headers['x-webhook-secret'];
if (secret !== process.env.ASSEMBLYAI_WEBHOOK_SECRET) {
console.warn('Webhook auth failed');
return res.status(401).json({ error: 'Unauthorized' });
}
const { transcript_id, status } = req.body;
console.log(`Webhook received: ${transcript_id} — ${status}`);
res.status(200).json({ : });
{
(status === ) {
transcript = client..(transcript_id);
(transcript);
} (status === ) {
(transcript_id, req..);
}
} (error) {
.(, error);
}
});
() {
.();
.();
.();
.();
(transcript. && transcript.. > ) {
{ response } = client..({
: [transcript.],
: ,
});
.(, response);
}
}
() {
.();
}
app.(, .());
Step 3: Webhook Endpoint (Next.js App Router)
import { AssemblyAI } from 'assemblyai';
import { NextRequest, NextResponse } from 'next/server';
const client = new AssemblyAI({
apiKey: process.env.ASSEMBLYAI_API_KEY!,
});
export async function POST(req: NextRequest) {
const secret = req.headers.get('x-webhook-secret');
if (secret !== process.env.ASSEMBLYAI_WEBHOOK_SECRET) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await req.json();
const { transcript_id, status } = body;
if (status === 'completed') {
const transcript = await client.transcripts.get(transcript_id);
console.log();
}
.({ : });
}
Step 4: Idempotent Processing
const processedTranscripts = new Set<string>();
async function idempotentProcess(transcriptId: string, handler: () => Promise<void>) {
if (processedTranscripts.has(transcriptId)) {
console.log(`Already processed: ${transcriptId}`);
return;
}
await handler();
processedTranscripts.add(transcriptId);
}
await idempotentProcess(transcript_id, async () => {
const transcript = await client.transcripts.get(transcript_id);
await processCompletedTranscript(transcript);
});
Step 5: Testing Webhooks Locally
ngrok http 3000
curl -X POST http://localhost:3000/webhooks/assemblyai \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: your-secret" \
-d '{
"transcript_id": "test-id-123",
"status": "completed"
}'
Webhook Payload Reference
AssemblyAI sends a POST with this JSON body:
{
"transcript_id": "6wij2z3g66-...",
"status": "completed"
}
For errors:
{
"transcript_id": "6wij2z3g66-...",
"status": "error",
"error": "Download error: unable to download audio from URL"
}
If redact_pii_audio was enabled, a second webhook fires when redacted audio is ready.
Output
- Webhook endpoint that receives transcription completion events
- Auth header verification for secure webhook handling
- Idempotent processing to handle retries
- LeMUR auto-analysis triggered on completion
Error Handling
| Issue | Cause | Solution |
|---|
| Webhook not received | URL not accessible from internet | Verify HTTPS URL, check firewall |
| 401 on webhook | Wrong auth header value | Match webhook_auth_header_value from submission |
| Duplicate processing | Webhook retried after timeout | Implement idempotency (check transcript_id) |
| Webhook timeout | Processing > 10 seconds | Return 200 immediately, process async |
| Missing transcript data | Fetching too early | Fetch with client.transcripts.get() after webhook |
Resources
Next Steps
For performance optimization, see assemblyai-performance-tuning.