Agent skill
Cartesia Webhooks Skill
Receive and authenticate Cartesia (Line voice agent) webhooks. Use when setting up a Cartesia webhook endpoint, verifying the x-webhook-secret header, debugging 401s from Cartesia, or handling call_started, call_completed, call_failed, call_turn and post_call_analysis events.
Install this skill
npx skills add hookdeck/webhook-skills --skill cartesia-webhooks
Cartesia sends webhook events to your HTTPS endpoint throughout a Line voice agent call: when it starts, each conversational turn, when it ends, and after post-call analysis. The payload is not signed. Cartesia returns the secret you set on the webhook, raw with no prefix, in the x-webhook-secret header of every delivery, and your endpoint compares it to its stored copy.
When to Use This Skill
- How do I receive Cartesia agent webhooks?
- How do I verify the
x-webhook-secretheader from Cartesia? - Does Cartesia sign webhook payloads? (No: it sends a shared secret)
- Why does my Cartesia webhook endpoint return 401?
- How do I handle
call_completed/post_call_analysisevents and avoid duplicates?
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyCartesiaRequest(headers, secret = process.env.CARTESIA_WEBHOOK_SECRET) {
if (!secret) return false; // never accept when unconfigured
const received = headers['x-webhook-secret']; // the secret itself, no prefix
return Boolean(received) && !Array.isArray(received) && safeEqual(received, secret);
}
Return 401 when this fails (Cartesia treats it as permanent and does not retry), 400 for a body that isn't a JSON object, and 200 once handled. There is no signature, so the raw body isn't needed for verification, but authenticate before parsing it. In Express, read the body with express.raw({ type: '*/*' }) so any content type reaches the handler.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
Every event has the envelope type, call_id, agent_id, webhook_id, webhook_request_id and timestamp, plus one typed field.
| Event | Sent When | Typed field |
|---|---|---|
call_started | Call session begins | call |
call_completed | Call ends normally | call |
call_failed | Call ends with an error | call |
call_turn | Each conversational turn | turn |
post_call_analysis | Async analysis finishes (summary) | analysis |
Cartesia retries up to 3 times on network errors and 403, 408, 429 and 5xx responses, so dedupe on webhook_request_id (stable across retries of one event).
Environment Variables
CARTESIA_WEBHOOK_SECRET=replace-with-a-long-random-secret # the secret set on the webhook
Local Development
Cartesia needs a public https URL. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 cartesia --path /webhooks/cartesia
Resources
- references/overview.md - How Cartesia webhooks work, events, payload fields
- references/setup.md - Create a webhook in the Playground or API and attach it to an agent
- references/verification.md - Shared-secret check, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests