Guide to CloudTalk Webhooks: Features and Best Practices
CloudTalk webhooks tell your systems what is happening on your phone lines as it happens: a call starts ringing, an agent picks up, a call ends, a recording or transcript becomes available, or a contact changes. If your team runs its calls and messages through CloudTalk, the cloud call-centre platform, webhooks are how that activity reaches your CRM, data warehouse or internal tools without polling the API.
This guide covers how CloudTalk webhooks are configured and signed, the events you can subscribe to and the shape of their payloads, how CloudTalk retries and disables failing endpoints, and the best practices for running a CloudTalk receiver in production.
What are CloudTalk webhooks?
A CloudTalk webhook is an endpoint you register in the CloudTalk Dashboard under Settings > Webhooks, with an HTTPS URL and a selection of event types. When one of those events occurs, CloudTalk sends a signed JSON POST to the URL. Every request has the same envelope, so one handler can route all of them on the body's type field.
CloudTalk delivers and signs these webhooks with the Svix scheme. Each request carries svix-id, svix-timestamp and svix-signature headers, and the signature is a base64 HMAC-SHA256 over the message ID, the timestamp and the raw body. Each endpoint has its own whsec_ signing secret.
The scheme only covers these account-level webhooks. Other CloudTalk features can also call a URL you own: the Webhook step in Call Flow Designer, the Send via Webhook option on an AI Voice Agent, and Workflow Automation actions. CloudTalk says those send their own payloads over their own delivery path, are not signed with this scheme, and do not get the retry, replay and retention behaviour described in this guide.
CloudTalk webhook features
| Feature | Details |
|---|---|
| Configuration | CloudTalk Dashboard (Settings > Webhooks, Admin role) or the API |
| Availability | Essential plan and higher |
| Subscription model | Choose event types per endpoint. An empty selection receives every type, including future ones |
| Signature headers | svix-id, svix-timestamp, svix-signature |
| Scheme | Svix: HMAC-SHA256, base64, entries prefixed with v1, |
| Signed content | {svix-id}.{svix-timestamp}.{raw body} |
| Secret | One whsec_ secret per endpoint, rotatable in the Dashboard or the API |
| Replay window | CloudTalk recommends rejecting timestamps more than 5 minutes from your server's time |
| Response window | Any 2xx within 15 seconds |
| Redirects | Not followed. A 3xx counts as a failed attempt |
| Retries | Up to 50 over about 11.5 hours |
| Auto-disable | After about 5 days of continuous failure, with automatic re-enabling if the endpoint recovers within 12 hours |
| Delivery guarantee | At-least-once, no ordering guarantee |
| Retention | Payloads kept for 30 days for Resend and Recover |
| Testing | Send test event on the endpoint's page |
CloudTalk publishes no source IP list for webhooks, so the signature is how you authenticate a request.
Common events
CloudTalk documents 22 event types and says new ones are added over time, so treat the list as a lower bound and acknowledge types you don't recognise.
| Area | Events | Fires when |
|---|---|---|
| Calls | call.started, call.ringing, call.answered, call.held, call.resumed, call.ended | A call begins, rings on a user, is picked up, goes on or off hold, or finishes with its final details |
| Call artefacts | call.recording_ready, call.tags_modified | A recording or voicemail is stored (asynchronously, after call.ended), or a call's tags change |
| Contacts | contact.created, contact.updated, contact.deleted | A contact is added, changed (with the full current profile) or removed (ID only) |
| Users | user.created, user.updated, user.deleted, user.status_changed | A user is added, changed or removed, or their availability changes |
| Messages | message.sent, message.received | An SMS, MMS or WhatsApp message is sent from or arrives on your number |
| Conversation Intelligence | transcript.ready, cidata.ready | A transcript is available, or AI analysis (summary, sentiment, topics, talk ratio, call score) is ready |
| AI Voice Agents | voice_ | An AI Voice Agent hands over extracted data |
| Dialer | dialer.disposition_, dialer.survey_filled | A user records a disposition or submits a survey response on a Dialer call |
Every payload uses the same envelope. Here is CloudTalk's call.ended example, trimmed:
{
"event_id": "1f3a9c2e-6b4d-8a2f-9c1d-7e5f3a2b4c6d",
"type": "call.ended",
"version": "v1",
"occurred_at": "2026-08-17T09:41:23.512Z",
"company_id": "123456",
"data": {
"call_id": "143223459",
"call_uuid": "018f4a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"direction": "incoming",
"external_number": "+12025550143",
"started_at": "2026-08-17T09:38:05Z",
"ended_at": "2026-08-17T09:41:23Z",
"talking_time": 154,
"waiting_time": 44,
"duration": 198,
"recorded": true,
"is_voicemail": false,
"contacts_total": 1,
"user": { "id": "705013", "name": "John Roe", "role": "agent" }
}
}
The envelope fields are always present:
event_idis a UUID that stays the same on every retry. CloudTalk tells you to deduplicate on it.typeis the event asresource.action. Route on it.versionis the payload version for that event type, such asv1. It changes only on breaking changes, and new optional fields can appear within a version.occurred_atis an RFC 3339 UTC timestamp. Delivery order is not guaranteed, so sort on it.company_idis your CloudTalk account ID.dataholds the event's details, and its shape depends ontypeandversion.
Inside data, fields with empty values are omitted rather than sent as null. Recordings and transcripts are not included inline: the events tell you they exist, and you fetch them through the CloudTalk API with your own credentials. Note that contact payloads carry their own data.type (contact or lead), which is not the event type.
Setting up CloudTalk webhooks
You need an Admin role and the Essential plan or higher to see Settings > Webhooks.
- Go to Settings > Webhooks in the CloudTalk Dashboard and click Add endpoint.
- Enter the HTTPS URL your endpoint listens on and, optionally, a description.
- Choose the event types to receive. Leave the selection empty to receive every type, including ones CloudTalk adds later.
- Open the new endpoint and reveal its signing secret. It looks like
whsec_. Store it server-side, for example asMfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw CLOUDTALK_WEBHOOK_SECRET. - Click Send test event, pick an event type, and check that your endpoint responds with a
2xx. The attempt appears in the endpoint's delivery log.
From the same page you can edit the URL or subscriptions, rotate the secret, disable or re-enable the endpoint, resend a single delivery, or recover every failed delivery since a time you choose. Everything except recovering a range of failed deliveries is also available through CloudTalk's API.
If you use Hookdeck, create a source with the CloudTalk source type, paste the whsec_ secret into it, and register the Hookdeck source URL as the endpoint in CloudTalk.
Securing CloudTalk webhooks
To verify a delivery, join svix-id, svix-timestamp and the raw request body with dots. Take the part of the secret after whsec_, base64-decode it, and use the bytes as the HMAC-SHA256 key. Encode the result as base64 and compare it in constant time with each v1, entry in the space-delimited svix-signature header. A match on any one entry is enough, which is what lets CloudTalk rotate a secret without downtime.
CloudTalk also recommends rejecting requests whose svix-timestamp is more than 5 minutes from your server's clock. That check is optional in the code below and off by default. If you turn it on, remember that a delivery replayed later, from CloudTalk's Resend or from Hookdeck, keeps its original timestamp.
const crypto = require('crypto');
const express = require('express');
function verifyCloudTalkSignature(rawBody, headers, secret, toleranceSeconds = 0) {
const id = headers['svix-id'];
const timestamp = headers['svix-timestamp'];
const signatureHeader = headers['svix-signature'];
if (!id || !timestamp || !signatureHeader || !secret || !secret.startsWith('whsec_')) return false;
if (!/^\d+$/.test(timestamp)) return false;
// Optional replay check, off by default.
if (toleranceSeconds > 0 && Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) {
return false;
}
const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
const expected = crypto
.createHmac('sha256', key)
.update(`${id}.${timestamp}.`)
.update(rawBody)
.digest();
return signatureHeader.split(' ').some((entry) => {
const [version, signature] = entry.split(',');
if (version !== 'v1' || !signature) return false;
const received = Buffer.from(signature, 'base64');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
const app = express();
app.post('/webhooks/cloudtalk', express.raw({ type: '*/*' }), (req, res) => {
if (!verifyCloudTalkSignature(req.body, req.headers, process.env.CLOUDTALK_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
// Deduplicate on event.event_id, route on event.type, then process async.
res.sendStatus(200);
});
The same check in Python:
import base64
import hashlib
import hmac
import os
import time
def verify_cloudtalk_signature(raw_body: bytes, headers, secret: str, tolerance_seconds: int = 0) -> 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 and secret.startswith("whsec_")):
return False
if not timestamp.isdigit():
return False
# Optional replay check, off by default.
if tolerance_seconds > 0 and abs(time.time() - int(timestamp)) > tolerance_seconds:
return False
key = base64.b64decode(secret[len("whsec_"):])
signed = f"{msg_id}.{timestamp}.".encode("utf-8") + raw_body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest())
for entry in signature_header.split(" "):
version, _, signature = entry.partition(",")
if version == "v1" and hmac.compare_digest(signature.encode("utf-8"), expected):
return True
return False
# In FastAPI: raw_body = await request.body()
# verify_cloudtalk_signature(raw_body, request.headers, os.environ["CLOUDTALK_WEBHOOK_SECRET"])
CloudTalk's docs also show verification with the open-source svix libraries. They work, with two caveats. The libraries always enforce a timestamp window, so they reject a delivery replayed later with its original timestamp. And in current versions (svix 2.x), verify() no longer returns the parsed payload, so code copied from examples that do event = wh.verify(...) gets undefined or None and has to parse the raw body itself.
To check your verifier, use the secret whsec_, the ID msg_2mKr8fQxLp7Ta3, the timestamp 1755423561 and this exact body:
{"event_id":"3f9a2b4c-8d1e-8a2b-9c3d-4e5f60718293","type":"contact.deleted","version":"v1","occurred_at":"2026-08-17T09:39:21.512Z","company_id":"123456","data":{"id":"44210"}}
The svix-signature header should be:
v1,Yz4/j9YvHgqtud+1rxpsa5hr8fUtmTOgULc9nw9sSk0=
Make CloudTalk webhooks production-ready. Hookdeck Event Gateway verifies the Svix signature on every CloudTalk delivery, accepts it well inside the 15-second window, and durably queues call, contact and transcript events before your service sees them.
CloudTalk webhook limitations and pain points
Only Settings > Webhooks is signed
The Problem: Requests from Call Flow Designer, an AI Voice Agent or Workflow Automation fail signature verification, or arrive in a shape your handler does not expect.
Why It Happens: CloudTalk has several features that call a URL you own, and only the account-level webhooks use the Svix scheme and the event envelope. The others send their own payloads over their own delivery path, without svix-* headers and without CloudTalk's retries, replay or retention.
Workarounds:
- Point the Call Flow Designer step, Voice Agent and Workflow Automation calls at a separate route, and authenticate them the way their own setup screens describe.
- Don't assume CloudTalk will retry those calls for you.
How Hookdeck Can Help: Use a CloudTalk source for the signed account-level webhooks, and a separate source for the unsigned feature calls, so each gets the right verification and every request still gets retries and replay in Hookdeck.
Duplicates and out-of-order events
The Problem: The same event is processed twice, or call.answered arrives before its call.started.
Why It Happens: Delivery is at-least-once, so a retry can race a slow but successful first attempt, and CloudTalk does not guarantee order. Resend and Recover can also redeliver events up to 30 days old.
Workarounds:
- Claim each
event_idin a store with a uniqueness constraint before you do the work, and keep the records for at least 30 days. - Sort by
occurred_atwhen sequence matters, and don't treat a later lifecycle event as an error when the earlier one hasn't arrived yet. - When you need the current state of a call or contact, fetch it from the API rather than rebuilding it from events.
How Hookdeck Can Help: Hookdeck's deduplication can drop repeated deliveries before they reach your service, based on fields you choose such as event_id, and its event log shows exactly what arrived and when.
Slow handlers lead to retries, then a disabled endpoint
The Problem: Events arrive more than once, and after a long outage they stop arriving at all.
Why It Happens: CloudTalk waits 15 seconds for a 2xx. Anything slower is a failed attempt, retried up to 50 times over about 11.5 hours. An endpoint that fails continuously for about 5 days is disabled. CloudTalk probes it for 12 hours and re-enables it automatically if it recovers, replaying what failed; after that you have to re-enable it yourself and use Recover.
Workarounds:
- Verify, persist the event, return
200, and process asynchronously. - Return
200for event types you don't handle, so they never count as failures. - Watch for CloudTalk's email to account admins when an endpoint is disabled.
How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives and holds it, so a slow or offline service on your side doesn't count against CloudTalk's retry budget or its disable timer. You then replay from Hookdeck on your own schedule.
Redirects count as failures
The Problem: Deliveries fail even though the URL works in a browser.
Why It Happens: CloudTalk does not follow 3xx redirects. A trailing-slash redirect, an HTTP-to-HTTPS redirect or a load balancer rewrite all count as failed attempts, and they count toward auto-disabling.
Workarounds:
- Register the final HTTPS URL exactly as your server serves it.
- Use a valid certificate. CloudTalk says expired or self-signed certificates cause failed deliveries.
How Hookdeck Can Help: A Hookdeck source URL answers directly with no redirect, and Hookdeck delivers onward to whatever destination URL you configure.
Verification fails on parsed bodies
The Problem: Every signature check fails, even with the right secret.
Why It Happens: The signature covers the exact bytes CloudTalk sent. Frameworks that parse JSON before your handler runs, or CSRF middleware that rejects unauthenticated POSTs, break verification. Using the whole whsec_... string as the key instead of its base64-decoded remainder fails the same way.
Workarounds:
- Read the raw body (
express.raw(),await request.body()) and parse only after verification. - Exempt the webhook route from CSRF protection, as CloudTalk recommends.
- Strip
whsec_and base64-decode the rest before using it as the key.
How Hookdeck Can Help: Hookdeck's CloudTalk source type handles the Svix verification, including multiple v1, entries during rotation, so your service receives requests that have already been checked.
Best practices
Route on type and tolerate the unknown
Switch on the envelope's type, and log and acknowledge anything you don't recognise. CloudTalk adds event types over time, and an empty subscription receives them automatically. New optional fields can also appear within a version, so don't reject payloads with unknown keys.
Treat missing fields as empty
Empty values are omitted from data, never sent as null. Use optional access, for example event.data?.user?.id, and don't require fields that only some calls have.
Fetch recordings and transcripts by reference
call.recording_ready and transcript.ready tell you an artefact exists. Fetch it with your own API credentials in a background job, and expect call.recording_ready to arrive some time after call.ended.
Subscribe only to what you use
High-frequency events such as user.status_changed add traffic to verify, queue and store. Select the event types you need rather than leaving the subscription empty, unless you want new types delivered automatically.
Acknowledge fast and process asynchronously
CloudTalk's 15-second window includes your processing time. Verify, persist, return 200, and do the real work afterwards. See why to process webhooks asynchronously.
Deduplicate on event_id
Record each event_id atomically before processing, for example with a unique index or Redis SET NX, and keep the records for the full 30-day replay window. See our guide to webhook idempotency.
Conclusion
CloudTalk signs its account-level webhooks with the Svix scheme: a base64 HMAC-SHA256 of {svix-id}.{svix-timestamp}.{raw body} in svix-signature, keyed with the decoded whsec_ secret. Verify against the raw body, accept any matching v1, entry, deduplicate on event_id, sort on occurred_at, and return a 2xx within 15 seconds so retries and auto-disabling never come into play. Keep the unsigned Call Flow Designer, Voice Agent and Workflow Automation calls on a separate route.
Hookdeck Event Gateway sits in front of your receiver, verifies every CloudTalk signature, and holds each event until your service has processed it, so an outage on your side costs latency instead of a disabled endpoint.
Get started with Hookdeck for free and handle CloudTalk webhooks reliably in minutes.