Gareth Wilson Gareth Wilson

Guide to Clickatell Webhooks: Features and Best Practices

Published


Clickatell webhooks keep your application in step with the messages you send and receive through One API, Clickatell's API for SMS and WhatsApp. When a message is delivered or fails, falls back to another channel, or a customer replies with text, media, a location or a button press, Clickatell calls your server with the details, so you can update delivery status, continue a conversation or hand it to an agent.

This guide covers how One API callbacks are configured and shaped, how to authenticate them with Custom Security Headers, how Clickatell handles failures, and the practices that keep a Clickatell receiver reliable.

What are Clickatell webhooks?

Clickatell calls them callbacks. A One API integration has two callback URLs: a Status callback URL for delivery updates and a Reply callback URL for inbound messages (mobile-originated, or "MO", in Clickatell's naming). Splitting them keeps conversations low-latency, because a burst of status updates doesn't queue up in front of a customer's reply.

Every callback is a JSON POST with the same three top-level fields: integrationId, integrationName and event. The kind of event is the name of the single key inside event, and its value is an array of events.

Clickatell doesn't sign callbacks. You authenticate them with credentials Clickatell sends on every request: Basic Auth configured on the callback URL, up to five Custom Security Headers, or both.

Clickatell webhook features

FeatureDetails
ProductOne API (SMS and WhatsApp). Legacy SMS Gateway callbacks are a separate product
ConfigurationOne API integration settings in the Clickatell portal
Callback URLsStatus callback URL and Reply callback URL, configured separately
Method and formatPOST, Content-Type: application/json
EnvelopeintegrationId, integrationName, event
Event kindThe name of the one key inside event
Batchingevent holds an array. Batch size is always 1 today, but may grow
AuthenticationBasic Auth, up to 5 Custom Security Headers per callback URL, or both
Header limitsNames up to 64 characters (letters, digits, -, _), values up to 512 characters
Signature and timestamp headerNone
AcknowledgementAny 2xx
RetriesAnything else except a redirect is retried. Slow responses are retried too
OrderingNot guaranteed. Use the millisecond timestamp fields
Source IPsNot published

Event kinds

Status and reply callbacks carry different kinds:

Key in eventCallbackWhat it carries
messageStatusUpdateStatuschannel, messageId, clientMessageId, status, statusCode, timestamp
messageFallbackStatusA message falling back to another channel, with status and statusDescription
statusIsTypingStatusA typing indicator from a user, with from
moTextReplyAn inbound text message
moMediaReplyAn inbound image, file or other media
moLocationReplyAn inbound location
moContactsReplyInbound contact cards
moSuggestedResponseReplyA tapped suggested response
moListPickerResponseReplyA selection from a list picker
moButtonResponseReplyA button press

channel is sms or whatsapp. Clickatell says it adds new kinds and fields as channels gain features, and asks you to make sure new event types and extra parameters don't break your processing.

A status callback for one delivered message has this shape:

{
  "integrationId": "1a2b3c4d5e6f",
  "integrationName": "Order notifications",
  "event": {
    "messageStatusUpdate": [
      {
        "channel": "whatsapp",
        "messageId": "f3a8a2c0e1c14b9e9d6b1c7a2d3e4f50",
        "clientMessageId": "order-1042",
        "status": "DELIVERED",
        "timestamp": 1761480000000
      }
    ]
  }
}

The values are illustrative: Clickatell's reference documents the schema, not a sample body, and the schema also allows a numeric statusCode. clientMessageId echoes the ID you supplied when sending, which makes it the easiest way to link a status back to your own record.

Inspect Clickatell callbacks as they arrive. Set a Hookdeck Inspect URL as a callback URL on a test integration to see the exact headers and body Clickatell sends, with no account or setup required.

Setting up Clickatell webhooks

Generate a long random value first, for example with openssl rand -hex 32, and save it somewhere safe. Clickatell encrypts header values and can't show them to you again.

  1. Log in to the Clickatell portal and open your One API integration settings.
  2. Set the Status Callback URL and, if you handle inbound messages, the Reply Callback URL.
  3. In the Custom Security Headers section for each callback URL, click Add Header, enter a name such as X-Webhook-Secret and your value, and click Save.
  4. Send a test message and confirm that the status callback arrives with your header.

If the Custom Security Headers section is missing, your user role needs the permission for it; ask Clickatell Support. Headers are configured per callback URL, so set the header on both URLs. You can use the same name on both.

Avoid naming the header Authorization unless you mean to: a custom Authorization header replaces any Basic Auth configured for that URL.

With Hookdeck. Clickatell is a supported source in Hookdeck. Its setup note reads: "Covers One API callbacks only. Add a Custom Security Header to the callback URL." Create a source with the Clickatell type and use its URL as the callback URL. Enter the header name in API Key Header and its exact value in API Key. Header names match case-insensitively; the value must match exactly. The source accepts POST only. Basic Auth on its own isn't checked by this source, so add a Custom Security Header even if you already use Basic Auth. A request with a missing or wrong header still gets a 200 from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.

For local development, hookdeck listen 3000 clickatell --path /webhooks/clickatell gives you a public URL that forwards to your machine.

Securing Clickatell webhooks

Read the header you configured and compare its whole value with the one you stored, in constant time. Reject the request if the header is missing.

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

const HEADER_NAME = (process.env.CLICKATELL_WEBHOOK_HEADER || 'x-webhook-secret').toLowerCase();
const SECRET = process.env.CLICKATELL_WEBHOOK_SECRET;

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/clickatell', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get(HEADER_NAME); // case-insensitive lookup
  if (!SECRET || typeof received !== 'string' || !safeEqual(received, SECRET)) {
    return res.sendStatus(401);
  }

  let body;
  try {
    body = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  // The kind is the key name; loop over every kind and every item,
  // because batches may grow beyond one event.
  for (const [kind, items] of Object.entries(body.event || {})) {
    for (const item of Array.isArray(items) ? items : []) {
      if (kind === 'messageStatusUpdate') {
        // Update the message with item.clientMessageId / item.messageId.
      } else if (kind === 'moText') {
        // Hand the reply to your conversation logic.
      }
    }
  }

  res.sendStatus(200);
});

The same check in Python:

import hmac
import os

HEADER_NAME = os.environ.get("CLICKATELL_WEBHOOK_HEADER", "X-Webhook-Secret")
SECRET = os.environ.get("CLICKATELL_WEBHOOK_SECRET", "")


def verify_clickatell_request(headers) -> bool:
    if not SECRET:
        return False  # never accept when unconfigured
    received = headers.get(HEADER_NAME)  # Starlette and Flask headers are case-insensitive
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), SECRET.encode("utf-8"))

A Custom Security Header is a shared secret. It authenticates the sender but not the body, so a modified payload still passes, and nothing stops a captured request from being replayed. Use HTTPS only and a long random value, and rotate it if it might have leaked.

Make Clickatell webhooks production-ready. Hookdeck Event Gateway checks your Custom Security Header, answers Clickatell straight away, and queues every status update and reply so you can retry and replay them.

Clickatell webhook limitations and pain points

Stored values can't be read back

The Problem: If you lose the header value, you can't look it up in the portal.

Why It Happens: Clickatell encrypts header values when you save them and never displays them again.

Workarounds:

  • Store the value in your secret manager before you save it in Clickatell.
  • If it's lost, update the header with a new value and change your receiver at the same time.

How Hookdeck Can Help: Hookdeck keeps every request, including those that fail verification, with full headers, so you can see what Clickatell sent while you put matching values in place.

Rotations aren't instant

The Problem: Right after you update a header value, some callbacks still carry the old one.

Why It Happens: Clickatell caches header values. Its docs say in-flight and immediately following requests can use the previous value for a brief period, typically seconds.

Workarounds:

  • Accept both the old and the new value for a few minutes during a rotation.
  • Rotate the status and reply URLs one at a time.

How Hookdeck Can Help: Failed-verification requests stay in Hookdeck's logs, so you can confirm when the old value stops arriving.

Duplicates and out-of-order callbacks

The Problem: A DELIVERED status can arrive before an earlier status, and the same callback can arrive twice.

Why It Happens: Clickatell retries callbacks that aren't acknowledged fast enough, and it says events can be out of sequence.

Workarounds:

  • Order by the millisecond timestamp in each item, not by arrival.
  • Deduplicate on messageId, status and timestamp together, since one message has several statuses.

How Hookdeck Can Help: Hookdeck's deduplication can drop repeated callbacks based on fields you choose before they reach your service.

The payload shape is designed to change

The Problem: A handler that expects exactly one event per callback, or a fixed set of kinds, breaks when Clickatell adds a kind or starts batching.

Why It Happens: The kind is a key name, not a field value, batch size is fixed at 1 only "for now", and new message types are added as channels evolve.

Workarounds:

  • Iterate over every key in event and every item in each array.
  • Log and acknowledge unknown kinds instead of returning an error.

How Hookdeck Can Help: Hookdeck stores every callback with its body, so you can replay one with a new kind against your updated handler.

Best practices

Use separate callback URLs

Keep status and reply callbacks on different URLs, or at least different paths, as Clickatell intends. Replies are time-sensitive; status updates can wait in a queue.

Set clientMessageId when sending

Status callbacks echo it, so you can match a delivery update to your own record without storing Clickatell's messageId first.

Acknowledge fast and process asynchronously

Clickatell retries callbacks your application doesn't answer quickly enough. Verify the header, store the body, return 200, and do the work afterwards. See why to process webhooks asynchronously.

Make handlers idempotent

Retries mean the same callback can arrive more than once. See our guide to webhook idempotency.

Conclusion

Clickatell One API sends status and reply callbacks as JSON, with the event kind as the key inside event and an array of events as its value. Callbacks aren't signed, so add a Custom Security Header with a long random value to each callback URL, compare it in full and in constant time, iterate over every kind and item, and order and deduplicate using timestamp and messageId.

Hookdeck Event Gateway checks that header, acknowledges Clickatell immediately, and lets you retry and replay callbacks, so a slow handler doesn't turn into duplicate deliveries.

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