Gareth Wilson Gareth Wilson

Guide to Cartesia Webhooks: Features and Best Practices

Published


Cartesia webhooks report what happens on calls handled by Line, Cartesia's voice agent platform: a call starts, each turn of the conversation, the call ends or fails, and a post-call summary is ready. If you run voice agents on Cartesia, webhooks are how transcripts, outcomes and summaries reach your CRM, analytics and follow-up workflows.

This guide covers how Cartesia webhooks are created and attached to agents, how to authenticate them, the five events and their envelope, Cartesia's retry rules, and the best practices for running a receiver in production.

What are Cartesia webhooks?

A Cartesia webhook is a URL and a secret. You create it once and attach it to one or more agents, and attaching it turns on delivery for every call those agents handle. Cartesia then sends an HTTPS POST with a JSON body at each point in the call lifecycle.

The body is not signed. Cartesia sends the secret you set, unchanged, in an x-webhook-secret header on every delivery, and your endpoint checks that it matches its stored copy. Cartesia's own examples do exactly that: compare the header with the secret and return 401 if it differs.

Cartesia webhook features

FeatureDetails
ConfigurationThe agent's Webhook settings in the Playground, or the Agents API
AttachmentA webhook is attached to agents through the agent's webhook_id. One webhook can serve several agents
Event selectionNone documented. An attached webhook receives every event type
Authenticationx-webhook-secret: <secret>, sent verbatim
SecretRequired when you create the webhook. Omitted from API responses
Body signatureNone
Envelopetype, call_id, agent_id, webhook_id, webhook_request_id, timestamp, plus one typed field
Deduplication keywebhook_request_id, stable across retries of one event
RetriesUp to 3 attempts on network errors and 403, 408, 429 or any 5xx
Permanent failuresOther 4xx responses, such as 400, 401 and 404
TimeoutNot documented
IP allowlistNone published

Common events

EventSent whenTyped field
call_startedA call session beginscall
call_turnEach conversational turn, from the agent or the userturn
call_completedA call ends normallycall
call_failedA call ends with an errorcall
post_call_analysisAsynchronous analysis finishes (currently the summary)analysis

The call object matches the response of Cartesia's Get Call endpoint: id, agent_id, agent_name, status, start_time, end_time, end_reason, transcript, telephony_params, deployment_region, dynamic_variables, summary and, on failed calls, error_message. Fields that do not have a value yet, such as end_time on call_started, may be missing.

Here is Cartesia's call_completed example, trimmed:

{
  "type": "call_completed",
  "call_id": "ac_sid_gqkgRWUz2u64qFUjA1mZyr",
  "agent_id": "agent_rwh4HGMgyhK7rM5ucVqbiC",
  "webhook_id": "agent_webhook_P3MgdLf1cpaucZJ7xWehCC",
  "webhook_request_id": "webhook_req_8h2NcVqbiCP3MgdLf1cpa",
  "end_reason": "client_hangup",
  "timestamp": "2026-04-16T01:08:50.061907836Z",
  "call": {
    "id": "ac_sid_gqkgRWUz2u64qFUjA1mZyr",
    "agent_name": "My Agent",
    "status": "completed",
    "start_time": "2026-04-16T01:08:37.413659Z",
    "end_time": "2026-04-16T01:08:50.036327Z",
    "end_reason": "client_hangup",
    "transcript": [
      { "role": "assistant", "text": "Hi there! How can I help you today?", "start_timestamp": 0.41, "end_timestamp": 3.2 },
      { "role": "user", "text": "I want to schedule an appointment.", "start_timestamp": 3.5, "end_timestamp": 5.8 }
    ]
  }
}

The top-level end_reason (and a top-level body on some events) are deprecated fields Cartesia still sends for backwards compatibility. Read call.end_reason instead.

call_turn events carry a turn with id, role (assistant or user), text, start and end times in seconds from the start of the call, was_interrupted, time-to-first-byte figures and any tool_calls. post_call_analysis carries an analysis.summary, and it is not sent when nobody spoke on the call.

Setting up Cartesia webhooks

Generate a long random secret first, for example with openssl rand -hex 32, and store it server-side as CARTESIA_WEBHOOK_SECRET. Cartesia leaves the secret out of every API response, so your copy is the only one you can read back.

In the Playground, open your agent's Webhook settings and enter an https URL and the secret.

With the API, create the webhook and attach it to an agent:

curl -X POST "https://api.cartesia.ai/agents/webhooks" \
  -H "X-API-Key: $CARTESIA_API_KEY" \
  -H "Cartesia-Version: 2026-03-01" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/webhooks/cartesia", "secret": "'"$CARTESIA_WEBHOOK_SECRET"'"}'

curl -X PATCH "https://api.cartesia.ai/agents/{agent_id}" \
  -H "X-API-Key: $CARTESIA_API_KEY" \
  -H "Cartesia-Version: 2026-03-01" \
  -H "Content-Type: application/json" \
  -d '{"webhook_id": "agent_webhook_..."}'

If you receive Cartesia webhooks through Hookdeck, enter the same secret in the Hookdeck source.

Securing Cartesia webhooks

Read x-webhook-secret, compare it with your stored secret in constant time, and reject the request if it is missing or different. Fail closed if the secret is not configured.

const crypto = require('crypto');
const express = require('express');

function verifyCartesiaSecret(headerValue, secret) {
  if (typeof headerValue !== 'string' || !secret) return false;
  const received = Buffer.from(headerValue, 'utf8');
  const expected = Buffer.from(secret, 'utf8');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/cartesia', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyCartesiaSecret(req.get('x-webhook-secret'), process.env.CARTESIA_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on event.webhook_request_id, route on event.type, then process asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import hmac
import os


def verify_cartesia_secret(header_value, secret) -> bool:
    if not header_value or not secret:
        return False
    return hmac.compare_digest(header_value.encode("utf-8"), secret.encode("utf-8"))


# verify_cartesia_secret(request.headers.get("x-webhook-secret"), os.environ["CARTESIA_WEBHOOK_SECRET"])

Cartesia's documented examples compare with != and !==. A constant-time comparison, as above, avoids leaking information about the secret through response timing.

Make Cartesia webhooks production-ready. Hookdeck Event Gateway checks the x-webhook-secret header, stores every call, turn and summary event, and retries delivery on your schedule rather than Cartesia's three attempts.

Cartesia webhook limitations and pain points

Three attempts, and most 4xx responses are final

The Problem: Call records and transcripts go missing after a short outage or a configuration slip.

Why It Happens: Cartesia tries a delivery up to 3 times, and only for network errors and 403, 408, 429 or 5xx responses. A 400, 401 or 404 is treated as permanent, so a rotated secret or a broken route loses every delivery on the first attempt.

Workarounds:

  • Return 200 as soon as the secret matches, and process afterwards.
  • Alert on 401 and 404 responses from your webhook route.
  • Recover missed calls from Cartesia's Get Call API.

How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives and stores it, then delivers to your service with automatic retries and replay, so a short outage on your side does not count against Cartesia's three attempts.

Every event, whether you want it or not

The Problem: A service that only needs the summary still receives every turn of every call.

Why It Happens: Attaching a webhook to an agent turns on all event types. Cartesia documents no per-type selection, and call_turn fires for each utterance.

Workarounds:

  • Acknowledge events you do not need with 200 and drop them.
  • Keep the handler cheap for call_turn, which is the most frequent event.

How Hookdeck Can Help: Hookdeck filters can drop or route events by type, so call_turn can go to analytics while post_call_analysis goes to your CRM, without your services seeing the rest.

A shared secret, not a signature

The Problem: Anyone with the secret can send fabricated call data, and the body has no integrity check of its own.

Why It Happens: Cartesia returns the secret in a header rather than signing the body.

Workarounds:

  • Use an HTTPS URL and keep the secret out of logs.
  • For high-value actions, confirm the call through the Get Call API.

How Hookdeck Can Help: Hookdeck checks the header on every request and rejects any that fail with a VERIFICATION_FAILED cause, so forged requests never become events for your services.

Best practices

Deduplicate on webhook_request_id

Cartesia keeps webhook_request_id the same across every attempt of one event and different between events, and recommends using it to skip duplicates. See our guide to webhook idempotency.

Route on type and read the typed field

Switch on type, then read call, turn or analysis. Ignore the deprecated top-level body and end_reason.

Do not wait for post_call_analysis

The summary may already be in call.summary on call_completed, and post_call_analysis is not sent when nobody spoke. Do not hold up call processing waiting for it.

Acknowledge fast and process asynchronously

Return 200 once the secret matches and the event is stored, then write transcripts or trigger follow-ups. See why to process webhooks asynchronously, and for voice and AI pipelines, Hookdeck for AI agents and LLMs.

Handle personal data carefully

Transcripts and dynamic_variables can contain what callers said and what your agent collected. Store them with the same controls as any other customer data.

Conclusion

Cartesia sends Line call events to a webhook you attach to your agents, with your secret in the x-webhook-secret header and no body signature. Compare the header in constant time, deduplicate on webhook_request_id, route on type, and answer quickly, because Cartesia makes at most three attempts and treats most 4xx responses as final.

Hookdeck Event Gateway checks the secret, stores every call event, and redelivers on your terms, so a short outage on your side does not cost you transcripts.

Get started with Hookdeck for free and handle Cartesia webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.