Guide to Ada Webhooks: Features and Best Practices
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
| Feature | Details |
|---|---|
| Configuration | The dashboard (Config > PLATFORM > Webhooks) or the Webhooks API |
| Subscription model | Per endpoint, by event type, optionally narrowed by Svix channel tags |
| Delivery platform | Svix |
| Signature headers | Not documented by Ada. Svix sends svix-id, svix-timestamp, svix-signature |
| Scheme | Not documented by Ada. Svix signs an HMAC-SHA256 over {svix-id}.{svix-timestamp}.{raw body}, base64-encoded, sent as v1,<signature> |
| Secret | One 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 window | None stated by Ada |
| Response window | A 2xx within 15 seconds |
| Retries | Immediately, then after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours |
| Auto-disable | After all deliveries to an endpoint fail for one week |
| Replay | Resend a single message, or recover failed messages for a time window |
| Custom headers | Static per-endpoint headers, set on the endpoint's Advanced tab |
| Source IPs | Svix'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.
| Event | Fires when |
|---|---|
v1.conversation.created | A new conversation is created for a supported channel |
v1.conversation.message | A new message is added to a supported channel |
v1.conversation.ended | A conversation ends, either through POST /v2/conversations/{conversation_ or when a Voice call disconnects |
v1.conversation.handoff.ended | A handoff is ended |
v1.end_user.created | A new user is created through any of Ada's supported channels |
v1.end_user.updated | A value on the end user's profile changes |
v1.audit_log.emitted | A configuration change is recorded in the Audit log |
v1.proactive_call.queued | A request to call an end user with Voice Proactive Outreach is accepted and queued |
v1.proactive_ | The outbound call is placed with the carrier |
v1.proactive_ | A person answers the call and its conversation exists |
v1.proactive_ | A voicemail system or answering machine answers the call, and no conversation is created |
v1.proactive_call.failed | The 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
- In the Ada dashboard, go to Config > PLATFORM > Webhooks and click Add Endpoint.
- 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.
- Click Create, open the endpoint and copy its signing secret. Store it server-side, for example as
ADA_WEBHOOK_SECRET. - 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_, svix-id msg_, 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-idheader, which identifies the message. - Make writes idempotent on the entity ID in
data, such asconversation_idorend_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
2xxstraight 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.