> ## Documentation Index
> Fetch the complete documentation index at: https://assemblyai.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Subscribe to session and call lifecycle events for your agents. AssemblyAI POSTs a signed JSON payload to your URL when each event fires.

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](/voice-agents/voice-agent-api/manage-agents).

**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.

<CodeGroup>
  ```bash cURL theme={null}
  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"
    }'
  ```

  ```python Python theme={null}
  # pip install requests
  import os
  import requests

  resp = requests.post(
      "https://agents.assemblyai.com/v1/webhook-subscriptions",
      headers={"Authorization": os.environ["ASSEMBLYAI_API_KEY"]},
      json={
          "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",
      },
  )
  resp.raise_for_status()
  print(resp.json()["id"])
  ```

  ```javascript Node.js theme={null}
  // Node 18+ has fetch built in
  const res = await fetch("https://agents.assemblyai.com/v1/webhook-subscriptions", {
    method: "POST",
    headers: {
      Authorization: process.env.ASSEMBLYAI_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      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",
    }),
  });
  const data = await res.json();
  console.log(data);
  ```
</CodeGroup>

```json theme={null}
{
  "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](#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.                                                                                     |

<Note>
  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.
</Note>

## 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:

```json theme={null}
{
  "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):

```json theme={null}
{
  "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:

<CodeGroup>
  ```python Python theme={null}
  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
  ```

  ```javascript Node theme={null}
  import crypto from "node:crypto";

  function verify(rawBody, signatureHeader, secret) {
    const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
    const a = Buffer.from(expected), b = Buffer.from(signatureHeader || "");
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }

  // Express: capture the raw body for verification
  app.post("/webhooks/assemblyai",
    express.raw({ type: "application/json" }),
    (req, res) => {
      if (!verify(req.body, req.get("X-AAI-Signature"), SECRET)) return res.sendStatus(401);
      const event = JSON.parse(req.body.toString());
      // ... handle event
      res.sendStatus(200);
    });
  ```
</CodeGroup>

## Manage subscriptions

<CodeGroup>
  ```bash cURL expandable theme={null}
  # 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"
  ```

  ```python Python expandable theme={null}
  # pip install requests
  import os
  import requests

  headers = {"Authorization": os.environ["ASSEMBLYAI_API_KEY"]}

  # List (paginated, newest first)
  resp = requests.get("https://agents.assemblyai.com/v1/webhook-subscriptions", headers=headers)
  resp.raise_for_status()
  print(resp.json())

  # Retrieve one
  resp = requests.get("https://agents.assemblyai.com/v1/webhook-subscriptions/{id}", headers=headers)
  resp.raise_for_status()
  print(resp.json())

  # Update: change the URL/events, pause, or rotate the secret (bumps secret_version)
  resp = requests.patch(
      "https://agents.assemblyai.com/v1/webhook-subscriptions/{id}",
      headers=headers,
      json={"enabled": False},
  )
  resp.raise_for_status()
  print(resp.json())

  # Delete
  resp = requests.delete("https://agents.assemblyai.com/v1/webhook-subscriptions/{id}", headers=headers)
  resp.raise_for_status()
  ```

  ```javascript Node.js expandable theme={null}
  // Node 18+ has fetch built in
  const auth = { Authorization: process.env.ASSEMBLYAI_API_KEY };

  // List (paginated, newest first)
  let res = await fetch("https://agents.assemblyai.com/v1/webhook-subscriptions", {
    method: "GET",
    headers: auth,
  });
  console.log(await res.json());

  // Retrieve one
  res = await fetch("https://agents.assemblyai.com/v1/webhook-subscriptions/{id}", {
    method: "GET",
    headers: auth,
  });
  console.log(await res.json());

  // Update: change the URL/events, pause, or rotate the secret (bumps secret_version)
  res = await fetch("https://agents.assemblyai.com/v1/webhook-subscriptions/{id}", {
    method: "PATCH",
    headers: { ...auth, "Content-Type": "application/json" },
    body: JSON.stringify({ enabled: false }),
  });
  console.log(await res.json());

  // Delete
  res = await fetch("https://agents.assemblyai.com/v1/webhook-subscriptions/{id}", {
    method: "DELETE",
    headers: auth,
  });
  ```
</CodeGroup>

`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.                                |
