# 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_agent.extracted_data_ready` | An AI Voice Agent hands over extracted data |
| Dialer | `dialer.disposition_created`, `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:

```json
{
  "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.

```javascript
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:

```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](/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 `POST`s, 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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### 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](/webhooks/guides/implement-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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle CloudTalk webhooks reliably in minutes.