Guide to AssemblyAI Webhooks: Features and Best Practices
AssemblyAI webhooks tell your application when a transcription job has finished. If you transcribe recorded calls, meetings or podcasts with AssemblyAI, a webhook lets you submit the audio, move on, and get notified when the transcript is ready, instead of polling the API. Streaming sessions can send a webhook too, with the complete transcript once the session ends.
This guide covers how AssemblyAI's per-request webhooks work, what the two payloads contain, how to authenticate deliveries with a header you define, AssemblyAI's retry rules, and the best practices for running a receiver in production.
What are AssemblyAI webhooks?
AssemblyAI webhooks are callbacks attached to individual requests. There is no dashboard where you register an endpoint and no list of event types. You add a webhook_url to a transcription request, and AssemblyAI sends one POST to that URL when the job is done.
For pre-recorded audio, the webhook is deliberately small. The body holds only the transcript ID and its status, and you fetch the transcript itself from the API. For streaming, the webhook arrives after the session terminates and carries the finalized turns.
AssemblyAI does not sign webhooks. To authenticate them, you set webhook_auth_header_name and webhook_ on the request, and AssemblyAI includes that header, exactly as given, on the delivery. Your endpoint compares it with the value it expects.
AssemblyAI webhook features
| Feature | Details |
|---|---|
| Configuration | Per request: webhook_url on POST /v2/transcript, or a query parameter on the streaming WebSocket URL |
| Deliveries | One POST per transcript or streaming session |
| Pre-recorded body | { "transcript_, with status completed or error |
| Streaming body | { "session_, the finalized turns |
| Event type | None. The body shape and status tell you what happened |
| Authentication | A header whose name and value you set per request. No signature |
| Metadata | Your own query parameters on webhook_url |
| Success response | A 2xx within 10 seconds |
| Retries | Up to 10 times, 10 seconds apart |
| 4xx responses | Treated as a permanent failure, with no retry |
| Delivery status | webhook_status_code on the transcript records your endpoint's response |
| Source IPs | 44.238.19.20 (US) and 54.220.25.36 (EU) |
Common events
AssemblyAI does not name events. There are two kinds of delivery.
Pre-recorded transcription. When a transcript finishes, the body is:
{
"transcript_id": "5552493-16d8-42d8-8feb-c2a16b56f6e8",
"status": "completed"
}
status is completed or error, and that is all the body contains: no text and no error message. Call GET https://api.assemblyai.com/v2/transcript/{transcript_ to get the transcript, or its error field when the status is error.
Streaming sessions. When a streaming session with a webhook_url ends, the body carries the session ID and its finalized turns, trimmed here:
{
"session_id": "273e79fd-99e9-4e1d-91da-90f56a132d01",
"messages": [
{
"type": "Turn",
"turn_order": 0,
"end_of_turn": true,
"turn_is_formatted": true,
"transcript": "Smoke from hundreds of wildfires in Canada is triggering air quality alerts throughout the US...",
"words": [
{ "start": 4880, "end": 5040, "text": "Smoke", "confidence": 0.76054, "word_is_final": true }
]
}
]
}
The two shapes are easy to tell apart: a pre-recorded delivery has transcript_id, and a streaming delivery has session_id.
To tie a delivery back to your own records, add query parameters to the webhook_url, for example https://example.com/webhooks/assemblyai?customer_. They come back on the request URL.
Setting up AssemblyAI webhooks
- Choose a header name, such as
X-Webhook-Secret, and generate a long random value, for example withopenssl rand -hex 32. Store both server-side. - Send them with every transcription request that should call back:
curl https://api.assemblyai.com/v2/transcript \
-H "authorization: $ASSEMBLYAI_API_KEY" \
-H "content-type: application/json" \
-d '{
"audio_url": "https://assembly.ai/wildfires.mp3",
"webhook_url": "https://example.com/webhooks/assemblyai",
"webhook_auth_header_name": "X-Webhook-Secret",
"webhook_auth_header_value": "'"$ASSEMBLYAI_WEBHOOK_SECRET"'"
}'
The SDKs take the same fields. In the Python SDK, use set_webhook() on the transcription config, and use submit() rather than transcribe() so the call returns without waiting for the result.
For streaming, add the same parameters to the WebSocket URL:
wss://streaming.assemblyai.com/v3/ws?sample_rate=16000&webhook_url=https://example.com/webhooks/assemblyai&webhook_auth_header_name=X-Webhook-Secret&webhook_auth_header_value=<value>
The webhook URL must be reachable from AssemblyAI's servers, so localhost will not work. For local development, hookdeck listen 3000 assemblyai --path /webhooks/assemblyai with the Hookdeck CLI gives you a public URL that forwards to your machine.
If you receive AssemblyAI webhooks through Hookdeck, enter the header name and value in the Hookdeck source's Header Name and Header Value fields. Because the header is set per request, every transcription request that targets the Hookdeck URL must send the same name and value. A request sent without them cannot pass.
Securing AssemblyAI webhooks
Look up the header you chose, compare its value with the one you expect in constant time, and reject the request if it is missing or different. Node lowercases incoming header names, so look the header up in lowercase. Fail closed if the name or value is not configured.
const crypto = require('crypto');
const express = require('express');
function verifyAssemblyAIHeader(headers, headerName, expectedValue) {
if (!headerName || !expectedValue) return false;
const value = headers[headerName.toLowerCase()];
if (typeof value !== 'string') return false;
const received = Buffer.from(value, 'utf8');
const expected = Buffer.from(expectedValue, 'utf8');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
const app = express();
app.post('/webhooks/assemblyai', express.raw({ type: '*/*' }), (req, res) => {
const ok = verifyAssemblyAIHeader(
req.headers,
process.env.ASSEMBLYAI_WEBHOOK_HEADER_NAME,
process.env.ASSEMBLYAI_WEBHOOK_SECRET
);
if (!ok) return res.sendStatus(401);
const payload = JSON.parse(req.body.toString('utf8'));
// payload.transcript_id (pre-recorded) or payload.session_id (streaming).
// Store it, return 200, then fetch the transcript asynchronously.
res.sendStatus(200);
});
The same check in Python:
import hmac
def verify_assemblyai_header(headers, header_name, expected_value) -> bool:
if not header_name or not expected_value:
return False
value = headers.get(header_name) # Starlette and Flask headers are case-insensitive
if value is None:
return False
return hmac.compare_digest(value.encode("utf-8"), expected_value.encode("utf-8"))
Be careful with the status code you return. AssemblyAI treats any 4xx as a permanent failure and does not retry it. That is right for a forged request, but if your own configuration is wrong (a rotated secret, a renamed environment variable), every genuine delivery is rejected once and lost. Watch for 401s from your endpoint, and recover by fetching transcripts by ID.
AssemblyAI also sends every webhook from two fixed IP addresses, so you can add an IP allowlist on top of the header check.
Make AssemblyAI webhooks production-ready. Hookdeck Event Gateway checks your AssemblyAI auth header, answers within AssemblyAI's 10-second window, and keeps every completion notice until your service has fetched the transcript.
AssemblyAI webhook limitations and pain points
A 4xx loses the notification
The Problem: A configuration mistake on your side means transcripts finish but your application never hears about them.
Why It Happens: AssemblyAI does not retry a 4xx response. A mismatched secret produces a 401, and that delivery is finished.
Workarounds:
- Alert on any
4xxyour webhook route returns. - Record every transcript ID you submit, and poll for any that are still pending after an expected time.
- Check
webhook_status_codeon the transcript to see what your endpoint answered.
How Hookdeck Can Help: Hookdeck accepts the delivery as soon as it arrives, so AssemblyAI sees success. A request that fails the header check is kept in Hookdeck's request log as rejected with VERIFICATION_FAILED, rather than vanishing, and accepted events can be retried or replayed to your service after you fix the problem.
Ten seconds to respond
The Problem: A handler that fetches the transcript before answering times out and receives the same webhook again.
Why It Happens: AssemblyAI expects a 2xx within 10 seconds, and retries up to 10 times, 10 seconds apart, when it does not get one.
Workarounds:
- Return
200straight after the header check, and fetch the transcript in a background job. - Make the handler idempotent on
transcript_idorsession_id.
How Hookdeck Can Help: Hookdeck answers AssemblyAI immediately and delivers to your service on its own schedule, with a maximum delivery rate if your transcript processing needs one.
No signature
The Problem: Anyone who learns the header value can post a fake completion notice.
Why It Happens: AssemblyAI authenticates with a static header you define, not a signature over the body.
Workarounds:
- Use HTTPS, and keep the header value out of logs.
- Treat the webhook as a notification only: fetch the transcript from the API with your API key, and check that the ID belongs to a job you submitted.
- Allowlist
44.238.19.20and54.220.25.36.
How Hookdeck Can Help: Hookdeck checks the header on every request before anything is queued for your service, so forged requests never reach your handlers.
No event types
The Problem: One endpoint receiving both pre-recorded and streaming callbacks has nothing to switch on.
Why It Happens: AssemblyAI names no events. The body shape is the only signal.
Workarounds:
- Use separate paths for pre-recorded and streaming webhooks.
- Or branch on whether the body has
transcript_idorsession_id.
How Hookdeck Can Help: Hookdeck filters can route on the request path or on which body fields are present, sending each kind of callback to its own destination.
Best practices
Set the auth header on every request
Authentication only exists if the request that created the job included webhook_auth_header_name and webhook_. Put them in the one function your code uses to submit transcripts, so no call site forgets them.
Fetch, do not trust, the body
The pre-recorded body is only an ID and a status. Use it as a signal to fetch the transcript with your API key. See handling thin events.
Acknowledge fast and process asynchronously
Return 200 within the 10-second window and do the transcript fetch, summarisation or indexing afterwards. See why to process webhooks asynchronously, and for AI pipelines, Hookdeck for AI agents and LLMs.
Deduplicate on the job ID
Retries can repeat a delivery. Key processing on transcript_id (with status) or session_id. See our guide to webhook idempotency.
Use query parameters for correlation
Add your own IDs to webhook_url so each delivery tells you which customer or job it belongs to without a database lookup.
Conclusion
AssemblyAI webhooks are one-off callbacks set per request. Pre-recorded deliveries carry only transcript_id and status, streaming deliveries carry the finalized turns, and neither is signed, so authentication comes from a header you define with webhook_auth_header_name and webhook_. Answer within 10 seconds, never let a configuration error turn into a 4xx that AssemblyAI will not retry, and fetch the transcript by ID.
Hookdeck Event Gateway checks your auth header, answers AssemblyAI immediately, and holds each notice until your service has processed it, so a slow fetch or a misconfiguration does not lose a transcript.
Get started with Hookdeck for free and handle AssemblyAI webhooks reliably in minutes.