ElevenLabs Webhooks & Events
Overview
ElevenLabs webhooks send HTTP POST notifications when async operations complete: transcription completion, post-call data from Conversational AI agents, and call initiation failures. Every delivery is signed with an HMAC-SHA256 signature you must verify before processing. This skill builds a secure endpoint that verifies signatures, routes events by type, and acks fast to avoid auto-disable.
Prerequisites
- ElevenLabs account (webhooks configured in Settings > Webhooks)
- HTTPS endpoint accessible from the internet
- Webhook secret (generated during webhook creation in dashboard)
Instructions
The full, copy-ready code for each step lives in references/implementation.md; per-event handlers live in references/examples.md. The high-level workflow:
- Know the event types — subscribe only to what you handle (table below).
- Create the webhook in the dashboard (Settings > Webhooks) and copy the HMAC secret.
- Verify the signature with HMAC-SHA256 over
"<timestamp>.<raw_body>", using a timing-safe compare and a 5-minute replay window. See the full verifier. - Handle the request with a raw body parser, ack
200immediately, then process asynchronously. See the Express handler. - Route events to per-type handlers. See handler examples.
- Guard against duplicates with idempotency keyed on the event ID. See idempotency.
- Test locally by tunneling with ngrok. See local testing.
Webhook event types
| Event Type | Payload | When Triggered |
|------------|---------|----------------|
| post_call_transcription | Full conversation transcript, analysis, metadata | After Conversational AI call ends |
| post_call_audio | Base64-encoded call audio, minimal metadata | After call ends (if audio recording enabled) |
| call_initiation_failure | Failure reason, metadata | When an outbound call fails to connect |
| speech_to_text.completed | Transcription result, word timestamps | Async STT job completes |
Signature verification skeleton
// src/elevenlabs/webhook-verify.ts — Header: t=<unix_ts>,v1=<hex_sig>
export function verifyWebhookSignature(rawBody, signatureHeader, secret) {
const parts = new Map(signatureHeader.split(",").map(p => {
const [k, ...v] = p.split("="); return [k, v.join("=")];
}));
const timestamp = parts.get("t"), signature = parts.get("v1");
if (Math.floor(Date.now() / 1000) - parseInt(timestamp) > 300) {
return { valid: false, reason: "Timestamp too old" }; // replay guard
}
const expected = crypto.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody.toString()}`).digest("hex");
return { valid: crypto.timingSafeEqual(
Buffer.from(signature, "hex"), Buffer.from(expected, "hex")) };
}
See references/implementation.md for the production-hardened version with full error handling.
Output
Applying this skill produces:
src/elevenlabs/webhook-verify.ts— reusable HMAC-SHA256 verifier with replay protection and timing-safe comparison.src/api/webhooks/elevenlabs.ts— Express route that verifies signatures, acks200immediately, and routes events to per-type handlers.- Per-event handler functions (
handleTranscription,handleCallAudio,handleCallFailure,handleSTTCompleted) extracting the fields each payload carries. - An idempotency wrapper keyed on event ID so retried deliveries are processed once.
At runtime a verified delivery returns { "received": true } with HTTP 200; a bad signature or expired timestamp returns HTTP 401 { "error": "Invalid signature" }.
Webhook Reliability
| Behavior | Detail |
|----------|--------|
| Retry policy | ElevenLabs retries failed deliveries |
| Auto-disable | After 10 consecutive failures AND 7+ days since last success |
| Timeout | Your endpoint must respond within a few seconds |
| Re-enable | Manually re-enable in dashboard after fixing the endpoint |
| Authentication | HMAC-SHA256 via ElevenLabs-Signature header |
Error Handling
| Issue | Cause | Solution |
|-------|-------|----------|
| Signature mismatch | Wrong secret or body parsing | Use express.raw(), verify secret matches dashboard |
| Webhook auto-disabled | 10+ consecutive failures | Fix endpoint, re-enable in dashboard |
| Duplicate events | Retried delivery | Implement idempotency with event ID tracking |
| Handler timeout | Slow processing | Return 200 immediately, process async |
| Replay attack | Old timestamp reused | Check timestamp age (reject > 5 min) |
Examples
Route a decoded event to the right handler:
switch (event.type || event.event_type) {
case "post_call_transcription": await handleTranscription(event); break;
case "post_call_audio": await handleCallAudio(event); break;
case "call_initiation_failure": await handleCallFailure(event); break;
case "speech_to_text.completed": await handleSTTCompleted(event); break;
default: console.log("Unhandled event type:", event.type);
}
Simulate a delivery locally with curl:
curl -X POST http://localhost:3000/webhooks/elevenlabs \
-H "Content-Type: application/json" \
-H "ElevenLabs-Signature: t=$(date +%s),v1=test" \
-d '{"type":"speech_to_text.completed","data":{"text":"Hello world"}}'
Full per-event handlers (transcript, audio, call-failure, STT) with the exact fields each payload carries are in references/examples.md.
Resources
- Post-Call Webhooks
- Webhook API Reference
- references/implementation.md — full verifier, Express handler, idempotency, ngrok testing
- references/examples.md — per-event handler examples
Next Steps
For performance optimization, see the elevenlabs-performance-tuning skill, which covers connection pooling and batching to keep webhook handlers fast enough to ack within the ElevenLabs timeout window.