# Guide to ElevenLabs Webhooks: Features and Best Practices

ElevenLabs webhooks notify your application when an async voice or call job reports back: a call transcription is ready, or a voice is scheduled for removal or removed. If you're building on ElevenLabs, webhooks are how these long-running jobs tell you they're done without polling.

This guide covers how ElevenLabs webhooks work, the events you'll handle, how to verify the `ElevenLabs-Signature` with the official SDK, and the best practices for production.

## What are ElevenLabs webhooks?

ElevenLabs webhooks are JSON POSTs delivered to a URL you configure. Each is signed with an `ElevenLabs-Signature` header formatted `t=<timestamp>,v0=<signature>`. The signature is an HMAC-SHA256 (hex) over `<timestamp> + "." + raw_body`, keyed with your webhook signing secret. The header can carry more than one `v0=` value, and any one matching is valid. ElevenLabs rejects deliveries outside a 30-minute window to guard against replay. The official `@elevenlabs/elevenlabs-js` SDK verifies all of this for you.

## ElevenLabs webhook features

| Feature | Details |
| --- | --- |
| Configuration | ElevenLabs dashboard > Settings > General > Webhooks |
| Signature header | `ElevenLabs-Signature`, formatted `t=<ts>,v0=<sig>` |
| Signature scheme | HMAC-SHA256 (hex) over `<timestamp>.<raw_body>` |
| Replay window | 30 minutes; multiple `v0=` values, any match is valid |
| SDK | `@elevenlabs/elevenlabs-js` `webhooks.constructEvent` (Node); manual HMAC for Python |
| Delivery | 200 expected within 10s; auto-disable after 10 consecutive failures; may arrive out of order |

## Common events

ElevenLabs has four event types:

| Event | Fires when |
| --- | --- |
| `post_call_transcription` | Call analysis and transcription complete |
| `voice_removal_notice` | A voice is scheduled for removal |
| `voice_removal_notice_withdrawn` | A voice removal notice is cancelled |
| `voice_removed` | A voice is removed from the account |

The payload envelope is `{ type, data, event_timestamp }`; branch on `type`.

## Setting up ElevenLabs webhooks

In the ElevenLabs dashboard, go to Settings > General > Webhooks, add your endpoint URL, select events, and create the webhook. Copy the signing secret immediately (it won't be shown again) into `ELEVENLABS_WEBHOOK_SECRET`. Install the current SDK with `npm install @elevenlabs/elevenlabs-js`, not the outdated `elevenlabs` v1.x package.

## Securing ElevenLabs webhooks

Use the official SDK's `webhooks.constructEvent`, which verifies the `ElevenLabs-Signature` and returns the parsed event (throwing on an invalid signature). It needs the raw request body, so capture it before parsing.

```javascript
const { ElevenLabsClient } = require("@elevenlabs/elevenlabs-js");

const elevenlabs = new ElevenLabsClient({ apiKey: process.env.ELEVENLABS_API_KEY || "webhook-only" });
const SECRET = process.env.ELEVENLABS_WEBHOOK_SECRET;

app.post("/webhooks/elevenlabs", express.raw({ type: "application/json" }), async (req, res) => {
  const signature = req.headers["elevenlabs-signature"];
  const rawBody = Buffer.isBuffer(req.body) ? req.body.toString("utf8") : req.body;
  try {
    const event = await elevenlabs.webhooks.constructEvent(rawBody, signature, SECRET);
    res.sendStatus(200); // acknowledge fast
    processQueue.add(event); // branch on event.type, async
  } catch {
    res.sendStatus(401); // invalid signature or stale timestamp
  }
});

```

The ElevenLabs Python SDK doesn't provide webhook verification, so verify manually, mirroring the `t=,v0=` scheme, the 30-minute window, and multiple signatures:

```python
import hashlib
import hmac
import os
import time

SECRET = os.environ["ELEVENLABS_WEBHOOK_SECRET"].encode()

def verify(raw_body: bytes, signature_header: str) -> bool:
    params = dict(kv.split("=", 1) for kv in (signature_header or "").split(",") if "=" in kv)
    timestamp = params.get("t", "")
    if abs(time.time() - int(timestamp)) > 1800:  # 30-minute window
        return False
    signed = (timestamp + ".").encode() + raw_body
    expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
    # The header may carry multiple v0= values; any match is valid
    provided = [v[3:] for v in (signature_header or "").split(",") if v.startswith("v0=")]
    return any(hmac.compare_digest(expected, sig) for sig in provided)

```

## ElevenLabs webhook limitations and pain points

### The signed message is `timestamp.body`

The Problem: The HMAC is over `<timestamp> + "." + raw_body`, not the body alone. Signing the body by itself never matches.

Why It Happens: ElevenLabs binds the timestamp into the signed message, Stripe-style.

Workarounds:

* Let the SDK reconstruct and verify it, or build `timestamp + "." + body` manually.

How Hookdeck Can Help: Hookdeck verifies the signature at the edge, so your app receives pre-verified events.

### Use the current SDK, not the old package

The Problem: The verification helper lives in `@elevenlabs/elevenlabs-js`. The outdated `elevenlabs` (v1.x) package doesn't provide it, and the Python SDK doesn't verify webhooks at all.

Why It Happens: The SDK was renamed, and Python lacks a verification helper.

Workarounds:

* Install `@elevenlabs/elevenlabs-js` for Node; verify manually in Python.

How Hookdeck Can Help: Hookdeck verifies at the edge, so a missing or renamed SDK isn't a blocker.

### The 30-minute replay window

The Problem: Deliveries older than 30 minutes are rejected. A slow queue, clock skew, or replaying old events for testing trips this.

Why It Happens: ElevenLabs enforces a 30-minute freshness window.

Workarounds:

* Keep server clocks in sync, and don't replay events older than the window into the verifier.

How Hookdeck Can Help: Hookdeck verifies freshness at the edge and durably queues events, so downstream slowness doesn't fail verification.

### Out-of-order delivery

The Problem: Events may arrive out of order, so a later state can land before an earlier one.

Why It Happens: Delivery isn't strictly ordered.

Workarounds:

* Order by `event_timestamp`, and make handlers idempotent.

How Hookdeck Can Help: Hookdeck durably queues events so your worker can reconcile ordering at its own pace.

## Best practices

### Verify with the official SDK over the raw body

Use `webhooks.constructEvent(rawBody, signature, secret)` in Node; verify the `t=,v0=` HMAC manually in Python.

### Respect the 30-minute window and multiple signatures

Reject stale deliveries, and accept if any `v0=` value matches.

### Order by event_timestamp

Events can arrive out of order, so sort on `event_timestamp` and dedupe.

### Acknowledge fast, process asynchronously

Return 200 within 10 seconds and defer work to a queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

ElevenLabs webhooks are verified with an `ElevenLabs-Signature` HMAC-SHA256 over `<timestamp>.<raw_body>`, formatted `t=<ts>,v0=<sig>` with a 30-minute window and possibly multiple signatures. Use the official `@elevenlabs/elevenlabs-js` `constructEvent` in Node (verify manually in Python), respect the freshness window, and order by `event_timestamp`.

[Hookdeck](https://hookdeck.com) verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle ElevenLabs webhooks reliably in minutes.