Gareth Wilson Gareth Wilson

Guide to CloudTalk Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationCloudTalk Dashboard (Settings > Webhooks, Admin role) or the API
AvailabilityEssential plan and higher
Subscription modelChoose event types per endpoint. An empty selection receives every type, including future ones
Signature headerssvix-id, svix-timestamp, svix-signature
SchemeSvix: HMAC-SHA256, base64, entries prefixed with v1,
Signed content{svix-id}.{svix-timestamp}.{raw body}
SecretOne whsec_ secret per endpoint, rotatable in the Dashboard or the API
Replay windowCloudTalk recommends rejecting timestamps more than 5 minutes from your server's time
Response windowAny 2xx within 15 seconds
RedirectsNot followed. A 3xx counts as a failed attempt
RetriesUp to 50 over about 11.5 hours
Auto-disableAfter about 5 days of continuous failure, with automatic re-enabling if the endpoint recovers within 12 hours
Delivery guaranteeAt-least-once, no ordering guarantee
RetentionPayloads kept for 30 days for Resend and Recover
TestingSend 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.

AreaEventsFires when
Callscall.started, call.ringing, call.answered, call.held, call.resumed, call.endedA call begins, rings on a user, is picked up, goes on or off hold, or finishes with its final details
Call artefactscall.recording_ready, call.tags_modifiedA recording or voicemail is stored (asynchronously, after call.ended), or a call's tags change
Contactscontact.created, contact.updated, contact.deletedA contact is added, changed (with the full current profile) or removed (ID only)
Usersuser.created, user.updated, user.deleted, user.status_changedA user is added, changed or removed, or their availability changes
Messagesmessage.sent, message.receivedAn SMS, MMS or WhatsApp message is sent from or arrives on your number
Conversation Intelligencetranscript.ready, cidata.readyA transcript is available, or AI analysis (summary, sentiment, topics, talk ratio, call score) is ready
AI Voice Agentsvoice_agent.extracted_data_readyAn AI Voice Agent hands over extracted data
Dialerdialer.disposition_created, dialer.survey_filledA 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_id is a UUID that stays the same on every retry. CloudTalk tells you to deduplicate on it.
  • type is the event as resource.action. Route on it.
  • version is the payload version for that event type, such as v1. It changes only on breaking changes, and new optional fields can appear within a version.
  • occurred_at is an RFC 3339 UTC timestamp. Delivery order is not guaranteed, so sort on it.
  • company_id is your CloudTalk account ID.
  • data holds the event's details, and its shape depends on type and version.

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.

  1. Go to Settings > Webhooks in the CloudTalk Dashboard and click Add endpoint.
  2. Enter the HTTPS URL your endpoint listens on and, optionally, a description.
  3. Choose the event types to receive. Leave the selection empty to receive every type, including ones CloudTalk adds later.
  4. Open the new endpoint and reveal its signing secret. It looks like whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw. Store it server-side, for example as CLOUDTALK_WEBHOOK_SECRET.
  5. 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_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw, 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_id in a store with a uniqueness constraint before you do the work, and keep the records for at least 30 days.
  • Sort by occurred_at when 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 200 for 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.


Gareth Wilson

Gareth Wilson

Product Marketing

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