# Clickatell Webhooks

Clickatell's One API (WhatsApp and SMS) posts JSON callbacks to your server: delivery status
updates to a **status callback URL** and inbound messages (replies) to a **reply callback
URL**. Callbacks are **not signed**. You authenticate them with a secret header that you add
yourself as a **Custom Security Header** on the callback URL.

Hookdeck's Clickatell source shows this setup note: "Covers One API callbacks only. Add a Custom Security Header to the callback URL."

Basic Auth alone and legacy SMS Gateway callbacks are not covered by Hookdeck's Clickatell source.

## When to Use This Skill

* How do I receive Clickatell One API webhooks (callbacks)?
* How do I authenticate Clickatell callbacks? (No signature: use a Custom Security Header)
* How do I handle `messageStatusUpdate`, `moText` or `moMedia` events?
* Why does my Clickatell callback endpoint return 401?
* How do I tell status callbacks from reply callbacks?

## Verification (core)

Add a Custom Security Header to the callback URL in the Clickatell portal, then compare the
**whole header value** with the value you stored. The header name is whatever you chose
(`CLICKATELL_WEBHOOK_HEADER`, default `Authorization`). Do not strip or parse a `Bearer `
prefix: the expected value is exactly what you typed, prefix included.

```javascript
const crypto = require('crypto');

function verifyClickatellRequest(headers, {
  name = process.env.CLICKATELL_WEBHOOK_HEADER || 'Authorization',
  expected = process.env.CLICKATELL_WEBHOOK_SECRET,
} = {}) {
  if (!expected) return false;                       // never accept when unconfigured
  const received = headers[name.toLowerCase()];      // Node lowercases header names
  if (!received) return false;
  const a = Buffer.from(received);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

```

* Return `401` when the header is missing or does not match, `400` for a body that isn't a
  JSON object, and any 2xx once handled (any 2xx acknowledges the callback).
* Read the body raw (`express.raw({ type: '*/*' })`) so any content type works, and authenticate
  before parsing it.
* **A static shared credential authenticates the sender, not the body, and gives no replay
  protection** (no signature, no timestamp). Use HTTPS and rotate the header value in the
  portal if it leaks.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/clickatell-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/clickatell-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/clickatell-webhooks/examples/fastapi/).

## Event Types

The body is `{ integrationId, integrationName, event: { <kind>: [ ... ] } }`. The event kind is
the **name of the key inside `event`**; each value is an array of events. Dispatch on that key.

| Kind (key in `event`) | Callback | Sent When |
| --- | --- | --- |
| `messageStatusUpdate` | Status | A message's status changes (e.g. `DEVICE_ACK`) |
| `messageFallback` | Status | A message falls back to another channel |
| `statusIsTyping` | Status | The user is typing |
| `moText` | Reply | An inbound text message |
| `moMedia` | Reply | An inbound image, file or other media |
| `moLocation` | Reply | An inbound location |
| `moContacts` | Reply | Inbound contact cards |
| `moSuggestedResponse` | Reply | A suggested-reply button is tapped |
| `moListPickerResponse` | Reply | A list item is selected |
| `moButtonResponse` | Reply | A button is tapped |

Clickatell adds new kinds and fields over time, so acknowledge kinds you don't handle. Events can
be duplicated (retries) or arrive out of order: deduplicate on `messageId` and order by
`timestamp` (milliseconds).

## Environment Variables

```bash
CLICKATELL_WEBHOOK_HEADER=Authorization   # name of the Custom Security Header (default: Authorization)
CLICKATELL_WEBHOOK_SECRET=your-header-value  # the exact, whole header value, e.g. "Bearer abc123"

```

## Local Development

Clickatell can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 clickatell --path /webhooks/clickatell

```

Use the printed URL as the callback URL in Clickatell.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/clickatell-webhooks/references/overview.md) - What One API callbacks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/clickatell-webhooks/references/setup.md) - Add callback URLs and a Custom Security Header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/clickatell-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/clickatell-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests