> elevenlabs-webhooks-events
Implement ElevenLabs webhook HMAC signature verification and event handling. Use when setting up webhook endpoints for transcription completion, call recording, or agent conversation events from ElevenLabs. Trigger: "elevenlabs webhook", "elevenlabs events", "elevenlabs webhook signature", "handle elevenlabs notifications", "elevenlabs post-call webhook", "elevenlabs transcription webhook".
curl "https://skillshub.wtf/jeremylongshore/claude-code-plugins-plus-skills/elevenlabs-webhooks-events?format=md"ElevenLabs Webhooks & Events
Overview
ElevenLabs webhooks send HTTP POST notifications when async operations complete. Supported event types include transcription completion, post-call data from Conversational AI agents, and call initiation failures. Webhooks use HMAC-SHA256 signatures for verification.
Prerequisites
- ElevenLabs account (webhooks configured in Settings > Webhooks)
- HTTPS endpoint accessible from the internet
- Webhook secret (generated during webhook creation in dashboard)
Instructions
Step 1: 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 |
Step 2: Webhook Setup
# Create webhook in ElevenLabs dashboard:
# Settings > Webhooks > Create Webhook
# - URL: https://your-app.com/webhooks/elevenlabs
# - Select event types to subscribe to
# - Copy the generated HMAC secret
Step 3: HMAC Signature Verification
// src/elevenlabs/webhook-verify.ts
import crypto from "crypto";
/**
* Verify the ElevenLabs-Signature header using HMAC-SHA256.
*
* Header format: t=<unix_timestamp>,v1=<hex_signature>
* Signed payload: "<timestamp>.<raw_body>"
*/
export function verifyWebhookSignature(
rawBody: string | Buffer,
signatureHeader: string,
secret: string
): { valid: boolean; reason?: string } {
if (!signatureHeader || !secret) {
return { valid: false, reason: "Missing signature header or secret" };
}
// Parse header: t=1234567890,v1=abcdef...
const parts = new Map(
signatureHeader.split(",").map(p => {
const [key, ...val] = p.split("=");
return [key, val.join("=")] as [string, string];
})
);
const timestamp = parts.get("t");
const signature = parts.get("v1");
if (!timestamp || !signature) {
return { valid: false, reason: "Malformed signature header" };
}
// Replay protection: reject if older than 5 minutes
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 300) {
return { valid: false, reason: `Timestamp too old: ${age}s` };
}
// Compute expected HMAC
const signedPayload = `${timestamp}.${rawBody.toString()}`;
const expected = crypto
.createHmac("sha256", secret)
.update(signedPayload)
.digest("hex");
// Timing-safe comparison
try {
const isValid = crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
return { valid: isValid };
} catch {
return { valid: false, reason: "Signature length mismatch" };
}
}
Step 4: Express Webhook Handler
// src/api/webhooks/elevenlabs.ts
import express from "express";
import { verifyWebhookSignature } from "../../elevenlabs/webhook-verify";
const router = express.Router();
// CRITICAL: Use raw body parser for signature verification
router.post("/webhooks/elevenlabs",
express.raw({ type: "application/json" }),
async (req, res) => {
const signature = req.headers["elevenlabs-signature"] as string;
const secret = process.env.ELEVENLABS_WEBHOOK_SECRET!;
const { valid, reason } = verifyWebhookSignature(req.body, signature, secret);
if (!valid) {
console.error("Webhook verification failed:", reason);
return res.status(401).json({ error: "Invalid signature" });
}
// Return 200 immediately to prevent webhook auto-disable
res.status(200).json({ received: true });
// Process asynchronously
const event = JSON.parse(req.body.toString());
processEvent(event).catch(err =>
console.error("Webhook processing failed:", err)
);
}
);
// Event routing
async function processEvent(event: any) {
const eventType = event.type || event.event_type;
switch (eventType) {
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:", eventType);
}
}
Step 5: Event Handlers
// Conversational AI post-call transcript
async function handleTranscription(event: any) {
const {
conversation_id,
transcript, // Full conversation text
analysis, // AI analysis of the call
metadata, // Custom metadata from agent config
recording_url, // Audio recording URL (if enabled)
} = event.data;
console.log(`[Transcript] Conversation ${conversation_id}`);
console.log(`Transcript: ${transcript?.substring(0, 200)}...`);
// Store in your database
// await db.conversations.upsert({ conversation_id, transcript, analysis });
}
// Post-call audio recording
async function handleCallAudio(event: any) {
const {
conversation_id,
audio_base64, // Base64-encoded audio of the full conversation
} = event.data;
if (audio_base64) {
const audioBuffer = Buffer.from(audio_base64, "base64");
console.log(`[Audio] Received ${audioBuffer.length} bytes for ${conversation_id}`);
// Save audio: await fs.writeFile(`recordings/${conversation_id}.mp3`, audioBuffer);
}
}
// Failed outbound call
async function handleCallFailure(event: any) {
const {
conversation_id,
failure_reason,
metadata,
} = event.data;
console.error(`[Call Failed] ${conversation_id}: ${failure_reason}`);
// Alert: await alerting.notify("Call initiation failed", { conversation_id, failure_reason });
}
// Async Speech-to-Text completion
async function handleSTTCompleted(event: any) {
const {
transcription_id,
text,
words, // Word-level timestamps
language,
} = event.data;
console.log(`[STT Complete] ${transcription_id}: ${language}`);
console.log(`Text: ${text?.substring(0, 200)}...`);
// Process transcription results
}
Step 6: Idempotency Protection
// Prevent duplicate processing if ElevenLabs retries delivery
const processedEvents = new Set<string>();
async function withIdempotency(
eventId: string,
handler: () => Promise<void>
): Promise<void> {
if (processedEvents.has(eventId)) {
console.log(`Event ${eventId} already processed, skipping`);
return;
}
await handler();
processedEvents.add(eventId);
// Clean up old entries (in production, use Redis with TTL)
if (processedEvents.size > 10000) {
const oldest = Array.from(processedEvents).slice(0, 5000);
oldest.forEach(id => processedEvents.delete(id));
}
}
Step 7: Local Testing with ngrok
# Expose local server to internet
ngrok http 3000
# Use the ngrok URL as webhook endpoint in ElevenLabs dashboard
# https://abc123.ngrok.io/webhooks/elevenlabs
# Test with curl (simulated event)
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"}}'
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) |
Resources
Next Steps
For performance optimization, see elevenlabs-performance-tuning.
> related_skills --same-repo
> fathom-cost-tuning
Optimize Fathom API usage and plan selection. Trigger with phrases like "fathom cost", "fathom pricing", "fathom plan".
> fathom-core-workflow-b
Sync Fathom meeting data to CRM and build automated follow-up workflows. Use when integrating Fathom with Salesforce, HubSpot, or custom CRMs, or creating automated post-meeting email summaries. Trigger with phrases like "fathom crm sync", "fathom salesforce", "fathom follow-up", "fathom post-meeting workflow".
> fathom-core-workflow-a
Build a meeting analytics pipeline with Fathom transcripts and summaries. Use when extracting insights from meetings, building CRM sync, or creating automated meeting follow-up workflows. Trigger with phrases like "fathom analytics", "fathom meeting pipeline", "fathom transcript analysis", "fathom action items sync".
> fathom-common-errors
Diagnose and fix Fathom API errors including auth failures and missing data. Use when API calls fail, transcripts are empty, or webhooks are not firing. Trigger with phrases like "fathom error", "fathom not working", "fathom api failure", "fix fathom".