Webhooks let your backend react to what your agents do without polling. Register a URL and the events you care about, and AssemblyAI sends a signed POST to that URL whenever one fires (when a call connects, a session ends, and so on). This is how you log calls, kick off post-call processing, or update your own systems for agents you created over REST.
Base URL: https://agents.assemblyai.com. Manage subscriptions with your API key in the Authorization header, same as the rest of the API.
Events
| Event | Fires when |
|---|
session.started | A voice agent session begins. |
session.completed | A session ends (delivered with final duration and close reason). |
call.connected | An inbound phone call connects to an agent. |
call.ended | A call ends. |
call.failed | A call fails to connect or errors out. |
Create a subscription
POST /v1/webhook-subscriptions. Subscribe to one or more events; scope to a single agent with agent_id, or omit it to receive events for all your agents.
curl -X POST https://agents.assemblyai.com/v1/webhook-subscriptions \
-H "Authorization: $ASSEMBLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/assemblyai",
"events": ["session.completed", "call.ended"],
"secret": "a-long-random-signing-secret-at-least-32-chars",
"agent_id": "7ad24396-b822-4dca-871a-be9cc4781cf9"
}'
{
"id": "0d75ebd7-937c-495c-b638-ba582ec05b39",
"agent_id": "7ad24396-b822-4dca-871a-be9cc4781cf9",
"url": "https://your-app.com/webhooks/assemblyai",
"events": ["session.completed", "call.ended"],
"enabled": true,
"secret_version": 1,
"created_at": "2026-06-10T14:16:59Z",
"updated_at": "2026-06-10T14:16:59Z"
}
| Field | Required | Notes |
|---|
url | Yes | Where deliveries are POSTed. Must be https and a public host. |
events | Yes | One or more of the events above. No duplicates. |
secret | Yes | Your signing secret, 32-256 printable ASCII chars (no whitespace). Used to sign every delivery; store it, since it’s never returned. |
agent_id | No | Scope to one agent. Omit for account-wide events. |
enabled | No | Defaults to true. Set false to pause deliveries. |
The response returns a secret_version (an integer that increments when you rotate the secret), never the secret itself. Keep your own copy of the secret to verify signatures.
Receiving a delivery
When an event fires, AssemblyAI sends a POST to your url with these headers:
| Header | Value |
|---|
X-AAI-Event | The event type, e.g. session.completed. |
X-AAI-Signature | sha256=<hex HMAC-SHA256 of the raw body, keyed by your secret>. |
X-AAI-Delivery-Id | Unique per delivery attempt; use it to dedupe. |
Session events carry a session object:
{
"event_id": "f1d2...",
"event": "session.completed",
"timestamp": "2026-06-10T14:20:31Z",
"session": {
"session_id": "sess_abc123",
"account_id": 325374,
"agent_id": "7ad24396-b822-4dca-871a-be9cc4781cf9",
"status": "completed",
"duration_seconds": 142.6,
"public_close_reason": "client_ended",
"s3_prefix": "…",
"created_at": "2026-06-10T14:18:08Z",
"ended_at": "2026-06-10T14:20:31Z"
}
}
Call events carry a call object (with recording/transcript links where available):
{
"event_id": "a93c...",
"event": "call.ended",
"timestamp": "2026-06-10T14:20:32Z",
"call": {
"call_id": "call_8b2b1e8c...",
"account_id": 325374,
"agent_id": "7ad24396-b822-4dca-871a-be9cc4781cf9",
"status": "completed",
"direction": "inbound",
"from_number": "+14155550123",
"to_number": "+19016659697",
"session_id": "sess_abc123",
"recording_url": "https://…",
"transcript_url": "https://…",
"created_at": "2026-06-10T14:18:05Z",
"ended_at": "2026-06-10T14:20:32Z"
}
}
Respond with a 2xx status promptly. Failed deliveries are retried with backoff, so make your handler idempotent: dedupe on event_id (or X-AAI-Delivery-Id).
Verify the signature
Always verify X-AAI-Signature before trusting a delivery. Compute HMAC-SHA256 over the exact raw request body (don’t re-serialize the JSON) using your subscription secret, and compare in constant time:
import hashlib, hmac
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header or "")
# Flask example
@app.post("/webhooks/assemblyai")
def handle():
if not verify(request.get_data(), request.headers.get("X-AAI-Signature"), SECRET):
return "", 401
event = request.get_json()
# ... handle event["event"], event["session"] / event["call"]
return "", 200
Manage subscriptions
# List (paginated, newest first)
curl https://agents.assemblyai.com/v1/webhook-subscriptions -H "Authorization: $ASSEMBLYAI_API_KEY"
# Retrieve one
curl https://agents.assemblyai.com/v1/webhook-subscriptions/{id} -H "Authorization: $ASSEMBLYAI_API_KEY"
# Update: change the URL/events, pause, or rotate the secret (bumps secret_version)
curl -X PATCH https://agents.assemblyai.com/v1/webhook-subscriptions/{id} \
-H "Authorization: $ASSEMBLYAI_API_KEY" -H "Content-Type: application/json" \
-d '{ "enabled": false }'
# Delete
curl -X DELETE https://agents.assemblyai.com/v1/webhook-subscriptions/{id} -H "Authorization: $ASSEMBLYAI_API_KEY"
PATCH accepts any of url, events, secret, enabled. Sending a new secret rotates it and increments secret_version.
Endpoint summary
| Method | Path | Description |
|---|
POST | /v1/webhook-subscriptions | Create a subscription. |
GET | /v1/webhook-subscriptions | List your subscriptions. |
GET | /v1/webhook-subscriptions/{id} | Retrieve one. |
PATCH | /v1/webhook-subscriptions/{id} | Update (url, events, secret, enabled). |
DELETE | /v1/webhook-subscriptions/{id} | Delete. |