Gareth Wilson Gareth Wilson

Guide to Ada Webhooks: Features and Best Practices

Published


Ada webhooks report what happens inside your AI customer service agent: a conversation starts or ends, a message is added, a handoff to a human agent finishes, an end user's profile changes, someone edits the agent's configuration, or a Voice Proactive Outreach call is queued, answered or fails. If you run customer service on Ada, webhooks are how that activity reaches your CRM, analytics pipeline or internal tooling without polling the API.

This guide covers how Ada webhooks are configured and signed, the events you can subscribe to and the shape of their payloads, the gaps in Ada's documentation that catch out new integrations, and the best practices for running an Ada receiver in production.

What are Ada webhooks?

An Ada webhook is an endpoint: a URL you control, subscribed to one or more event types. You create endpoints in the Ada dashboard or through Ada's Webhooks API, and Ada says that webhooks created via the API "are identical to those created in the dashboard". When a subscribed event happens, Ada sends a JSON POST to the endpoint.

Ada does not run its own delivery system. Its documentation says that "webhook deliveries originate from a dedicated infrastructure, not directly from Ada's servers", points to Svix's source IP list, and uses Svix features such as channel tags and Svix Play. That matters for verification, because Ada's own pages say only that "each webhook endpoint has a unique signing secret" and never name the signature headers or the algorithm. Those details come from Svix: every request carries svix-id, svix-timestamp and svix-signature, and the signature is an HMAC-SHA256 over the message ID, the timestamp and the raw body.

Ada webhook features

FeatureDetails
ConfigurationThe dashboard (Config > PLATFORM > Webhooks) or the Webhooks API
Subscription modelPer endpoint, by event type, optionally narrowed by Svix channel tags
Delivery platformSvix
Signature headersNot documented by Ada. Svix sends svix-id, svix-timestamp, svix-signature
SchemeNot documented by Ada. Svix signs an HMAC-SHA256 over {svix-id}.{svix-timestamp}.{raw body}, base64-encoded, sent as v1,<signature>
SecretOne per endpoint. Ada doesn't document the format; Svix secrets use the whsec_ prefix. Read or rotate it in the dashboard or with GET /api/v2/webhooks/{id}/secret/
Replay windowNone stated by Ada
Response windowA 2xx within 15 seconds
RetriesImmediately, then after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours
Auto-disableAfter all deliveries to an endpoint fail for one week
ReplayResend a single message, or recover failed messages for a time window
Custom headersStatic per-endpoint headers, set on the endpoint's Advanced tab
Source IPsSvix's published list for your region (US for *.ada.support agents, EU for *.ada-eu.support)

Common events

Ada's overview lists 11 supported events, and a twelfth has its own reference page.

EventFires when
v1.conversation.createdA new conversation is created for a supported channel
v1.conversation.messageA new message is added to a supported channel
v1.conversation.endedA conversation ends, either through POST /v2/conversations/{conversation_id}/end/ or when a Voice call disconnects
v1.conversation.handoff.endedA handoff is ended
v1.end_user.createdA new user is created through any of Ada's supported channels
v1.end_user.updatedA value on the end user's profile changes
v1.audit_log.emittedA configuration change is recorded in the Audit log
v1.proactive_call.queuedA request to call an end user with Voice Proactive Outreach is accepted and queued
v1.proactive_call.initiatedThe outbound call is placed with the carrier
v1.proactive_call.answeredA person answers the call and its conversation exists
v1.proactive_call.machine_detectedA voicemail system or answering machine answers the call, and no conversation is created
v1.proactive_call.failedThe call ends without a person answering. failure_reason says why

The v1.proactive_call.* events are only available to AI Agents with Voice Proactive Outreach. v1.conversation.handoff.ended has a reference page but no row in the supported events table, so check that it is offered for your endpoint before you depend on it. Ada also exposes GET /api/v2/webhooks/event-types/ for the live list.

Every payload has the same four top-level fields: type is the event name, timestamp is when the event was generated (with millisecond precision, which Ada says is there "to help with event ordering"), data holds the event's fields, and the optional tags array carries filter tags such as the end_user_id. Ada documents each event as a field schema and publishes no example bodies, so the following v1.conversation.created payload is assembled from that schema:

{
  "type": "v1.conversation.created",
  "timestamp": "2026-10-21T09:30:00.123Z",
  "data": {
    "conversation_id": "6512b3a1c6f84d0012ab34cd",
    "channel_id": "5df263b7db5a7e6ea03fae9b",
    "created_at": "2026-10-21T09:30:00.120Z",
    "end_user_id": "6512b3a1c6f84d0012ab34ce",
    "metadata": {},
    "ai_agent_domain": "example.ada.support"
  }
}

There is no event ID in the body. The svix-id header identifies the message and is the value to deduplicate on.

Setting up Ada webhooks

  1. In the Ada dashboard, go to Config > PLATFORM > Webhooks and click Add Endpoint.
  2. Enter your HTTPS endpoint URL and select the event types you want. Ada suggests starting with all event types while testing, then narrowing the list for production.
  3. Click Create, open the endpoint and copy its signing secret. Store it server-side, for example as ADA_WEBHOOK_SECRET.
  4. Optionally, add channel values in the endpoint's Channels field, so it only receives events tagged with those values (a custom channel ID, a handoff integration name such as zendesk_chat, or your SMS channel ID).

To manage endpoints from code, use the Webhooks API with an Ada API key. It supports creating, listing, updating and deleting endpoints, retrieving and rotating secrets, and listing event types. GET https://<your-agent>.ada.support/api/v2/webhooks/{id}/secret/ returns the secret in a key field.

If you don't have a URL ready, the Use Svix Play button in the webhooks UI generates a test URL and logs every delivery. Ada says Svix Play is strictly for testing.

Securing Ada webhooks

Ada doesn't name the headers or the signing scheme, so these steps follow Svix's documentation. Read the three Svix headers, base64-decode the part of your secret after Svix's whsec_ prefix, compute an HMAC-SHA256 of {svix-id}.{svix-timestamp}.{raw body}, base64-encode it, and compare it in constant time with each v1, entry in svix-signature. The header can hold several space-separated entries, and a request is valid if any of them matches.

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

function verifyAdaSignature(rawBody, headers, secret) {
  const id = headers['svix-id'];
  const timestamp = headers['svix-timestamp'];
  const signatureHeader = headers['svix-signature'];
  if (!id || !timestamp || !signatureHeader || !secret) return false;

  // Optional: Ada states no tolerance. If you want a freshness check, add it here, e.g.
  // if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  // The key is the base64-decoded part of the secret after "whsec_".
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const signedContent = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
  const expected = crypto.createHmac('sha256', key).update(signedContent).digest();

  // svix-signature is a space-separated list of "v1,<base64>" entries.
  return signatureHeader.split(' ').some((entry) => {
    const [version, value] = entry.split(',');
    if (version !== 'v1' || !value) return false;
    const received = Buffer.from(value, 'base64');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

const app = express();

app.post('/webhooks/ada', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.ADA_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyAdaSignature(req.body, req.headers, secret)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on req.headers['svix-id'], route on event.type, then process asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import base64
import hashlib
import hmac


def verify_ada_signature(raw_body: bytes, headers, secret: str) -> bool:
    msg_id = headers.get("svix-id")
    timestamp = headers.get("svix-timestamp")
    signature_header = headers.get("svix-signature")
    if not (msg_id and timestamp and signature_header and secret):
        return False

    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed_content = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed_content, hashlib.sha256).digest())

    for entry in signature_header.split(" "):
        version, _, value = entry.partition(",")
        if version == "v1" and hmac.compare_digest(value.encode(), expected):
            return True
    return False


# In FastAPI: verify_ada_signature(await request.body(), request.headers,
#                                  os.environ["ADA_WEBHOOK_SECRET"])

If you'd rather use a library, the standardwebhooks package (npm and PyPI) implements the same scheme. It expects webhook-* header names, so pass the svix-* values under those names. It also rejects timestamps more than five minutes from your server's clock, which is the library's default rather than anything Ada asks for.

To check your verifier, use the secret whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw, svix-id msg_2KWPBgLlAfxdpx2AI54pPJ85f4W, svix-timestamp 1761039000 and this exact body:

{"type":"v1.conversation.created","timestamp":"2026-10-21T09:30:00.123Z","data":{"conversation_id":"conv_123","end_user_id":"eu_456"}}

The svix-signature should be:

v1,XIaNDTwBWOQ0KHkOZ9TsBUKuK1JC0l+D98OWoBkaF7w=

Make Ada webhooks production-ready. Hookdeck Event Gateway verifies the Svix signature on every Ada delivery, accepts it within Ada's 15-second window, and durably queues each conversation and end user event before your service sees it.

Ada webhook limitations and pain points

The signature scheme isn't on Ada's pages

The Problem: You read Ada's webhook documentation looking for the signature header and find a secret but no instructions for using it.

Why It Happens: Ada hands delivery to Svix and documents its endpoints, events and retries, but not the signing scheme. The headers, the signed content and the whsec_ key format are all in Svix's documentation. Svix also lets senders rename its headers to webhook-*, and Ada doesn't say which prefix its deliveries use.

Workarounds:

  • Implement the Svix scheme as shown above, or use a Standard Webhooks library.
  • Log the header names of your first real delivery to confirm the svix- prefix.
  • Fail closed if the signature headers are missing, rather than skipping verification.

How Hookdeck Can Help: Hookdeck's Ada source type implements the Svix scheme. It reads the svix-* headers and falls back to webhook-*, so either prefix verifies against your whsec_ secret.

No event ID in the payload

The Problem: A retried delivery looks like a new event, and handlers that create records end up with duplicates.

Why It Happens: Ada's body is type, timestamp, data and tags. None of them identifies the event. Retries, resends and recoveries all deliver the same message again.

Workarounds:

  • Deduplicate on the svix-id header, which identifies the message.
  • Make writes idempotent on the entity ID in data, such as conversation_id or end_user_id.

How Hookdeck Can Help: Hookdeck's deduplication can drop repeated deliveries based on a header such as svix-id or on fields in the body, before they reach your service.

No example payloads

The Problem: You write a handler against the documented fields and find out what a real body looks like only when the first event arrives.

Why It Happens: Each event's reference page lists fields, types and descriptions, and marks every field as optional, but none shows a complete body.

Workarounds:

  • Point a test endpoint (or Svix Play) at your agent and capture one delivery per event type before building handlers.
  • Treat every field as optional, as the schema says.

How Hookdeck Can Help: Every request that reaches a Hookdeck source is stored with its headers and body, so you can inspect real Ada payloads and replay them against your code while you build.

A week of failures disables the endpoint

The Problem: After a long outage, events stop arriving and nothing resumes when your service comes back.

Why It Happens: Ada retries failed deliveries with exponential backoff, and its overview describes automatic retries "up to a week". If all attempts to an endpoint fail for one week, Ada disables it, and it stays off until someone re-enables it in the Webhooks UI.

Workarounds:

  • Respond with a 2xx straight after verification and process asynchronously.
  • After an outage, use Options > Recover Failed Messages on the endpoint page to replay everything since a point in time.
  • Check the endpoint's status in the dashboard as part of incident recovery.

How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives and holds it, so your downtime doesn't count against Ada's retry schedule. You can then retry or replay the events into your service on your own schedule.

The audit log webhook is best-effort

The Problem: A configuration change is missing from your audit trail.

Why It Happens: Ada describes v1.audit_log.emitted delivery as best-effort and points to the Audit log pull API (GET /api/v2/analytics/audit-log/events/) as the durable record.

Workarounds:

  • Treat the webhook as a prompt to look, and reconcile periodically against the pull API.

How Hookdeck Can Help: Hookdeck keeps every audit log event it receives, with its full payload, so the deliveries that did arrive are searchable alongside your reconciliation.

Best practices

Verify against the raw body

The HMAC covers the exact bytes Ada sent. Use express.raw() in Express, Buffer.from(await request.arrayBuffer()) in a Next.js route handler, or await request.body() in FastAPI, and parse the JSON only after verification.

Route on type

Every subscribed event arrives at the same endpoint, so switch on type and acknowledge any value you don't recognise with a 2xx. That way a new event type never turns into a failed delivery.

Order by timestamp, not arrival

Retries and recoveries can deliver an older event after a newer one. Use the body's timestamp to decide which update is the latest, for example when applying v1.end_user.updated events to a profile.

Use channel filtering instead of filtering in code

If one AI Agent feeds several integrations, give each integration its own endpoint with a Channels filter. Svix drops events that don't match before delivery, so each endpoint only receives the events it needs.

Acknowledge fast and process asynchronously

Ada counts anything slower than 15 seconds as a failure. Verify, store the event, return 200, and do the work afterwards. See why to process webhooks asynchronously and our guide to webhook idempotency.

Rotate secrets without downtime

After you rotate a secret, svix-signature can carry more than one v1, entry. Accept a request if any entry matches, and update your stored secret straight after rotating.

Conclusion

Ada signs every webhook through Svix, and Svix's documentation gives the scheme: an HMAC-SHA256 of the message ID, timestamp and raw body, keyed with the base64-decoded whsec_ secret and sent in svix-signature. Verify against the raw bytes, deduplicate on svix-id because the body has no event ID, route on type, order by timestamp, and respond within 15 seconds so a long outage doesn't end with a disabled endpoint.

Hookdeck Event Gateway sits in front of your receiver, checks each Svix signature, and stores every Ada event until your service has processed it.

Get started with Hookdeck for free and handle Ada 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.