# 360dialog Webhooks

360dialog (WhatsApp Business API provider) sends webhooks to your server for inbound WhatsApp
messages, message status changes and errors. Bodies follow Meta's Cloud API format. Webhooks
are **not signed**: the only authentication is an `Authorization: Basic base64(USER:PASS)`
header that you set yourself in the `headers` object when you set the webhook URL.

## When to Use This Skill

* How do I receive 360dialog webhooks?
* How do I authenticate 360dialog webhooks with Basic auth?
* Does 360dialog sign webhook payloads? (No: static Basic credential only)
* Why does my 360dialog webhook endpoint return 401?
* How do I tell inbound messages from status updates (`messages` vs `statuses`)?
* How do I handle Coexistence events (`smb_message_echoes`, `history`, `smb_app_state_sync`)?

## Verification (core)

Set the webhook URL with the 360dialog API, passing the credential in `headers`:

```json
{ "url": "https://example.com/webhooks/360dialog",
  "headers": { "Authorization": "Basic dGVzdHVzZXI6dGVzdHBhc3M=" } }

```

360dialog then sends that header on every request. Decode it, split on the **first** colon,
and compare the username and the password each in constant time, evaluating both:

```javascript
const crypto = require('crypto');
const sha = (b) => crypto.createHash('sha256').update(b).digest(); // equal-length buffers
const safeEqual = (a, b) => crypto.timingSafeEqual(sha(a), sha(b));

function verify360dialogRequest(headers, user = process.env.THREE_SIXTY_DIALOG_WEBHOOK_USERNAME,
                                pass = process.env.THREE_SIXTY_DIALOG_WEBHOOK_PASSWORD) {
  if (!user || !pass) return false;                    // never accept when unconfigured
  const m = /^basic\s+([A-Za-z0-9+/]+={0,2})\s*$/i.exec(headers['authorization'] || '');
  if (!m) return false;                                // scheme is case-insensitive
  const decoded = Buffer.from(m[1], 'base64');
  const i = decoded.indexOf(0x3a);                     // first ':' (passwords may contain ':')
  if (i === -1) return false;
  const userOk = safeEqual(decoded.subarray(0, i), Buffer.from(user));
  const passOk = safeEqual(decoded.subarray(i + 1), Buffer.from(pass));
  return userOk && passOk;                             // no short-circuit between the two
}

```

Return `401` when this fails, `400` for a body that isn't a JSON object, and `200` once handled
(360dialog needs a `200` within 5 seconds or it retries). Read the body raw
(`express.raw({ type: '*/*' })`) and parse JSON only after the check.
Python and Next.js equivalents are in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/360dialog-webhooks/references/verification.md).

**A static shared credential authenticates the sender, not the body, and gives no replay
protection (no signature, no timestamp).** Anyone who learns it can forge requests, and a
captured request can be replayed. Use HTTPS, treat the credential like a password, rotate it
(set the webhook URL again with new headers) if it leaks, and dedupe on message IDs.

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

## Event Types

Bodies are `{ object: "whatsapp_business_account", entry: [{ id, changes: [{ field, value }] }] }`.
Dispatch on `entry[].changes[].field`, and within `messages` on which array is present:

| `field` | Present in `value` | Meaning |
| --- | --- | --- |
| `messages` | `messages` | Inbound message; `messages[].type` is `text`, `image`, `video`, `audio`, `document`, `sticker`, `location`, `contacts`, `reaction`, `interactive`, `button`, `order`, `system`, `unsupported`, ... |
| `messages` | `statuses` | Outbound message status: `sent`, `delivered`, `read`, `failed`, `played` |
| `messages` | `errors` | Operational or delivery errors outside the normal message flow |
| `smb_message_echoes` | `message_echoes` | Coexistence: messages the business sent from the WhatsApp Business app |
| `history` | `history` | Coexistence: chat history shared after onboarding |
| `smb_app_state_sync` | `state_sync` | Coexistence: business contacts added or changed |

The docs show `history` and `smb_app_state_sync` as `{ id, event, data }` envelopes (partner
webhooks); the examples accept both shapes.

## Environment Variables

```bash
THREE_SIXTY_DIALOG_WEBHOOK_USERNAME=your_webhook_username   # USER in Basic base64(USER:PASS)
THREE_SIXTY_DIALOG_WEBHOOK_PASSWORD=your_webhook_password   # PASS

```

## Local Development

```bash
npx hookdeck-cli listen 3000 360dialog --path /webhooks/360dialog

```

No account required. Use the printed URL as the `url` when setting the webhook, together
with your `Authorization` header.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/360dialog-webhooks/references/overview.md) - Payload format, message types, statuses, Coexistence events
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/360dialog-webhooks/references/setup.md) - Setting the webhook URL and Basic auth headers with the API
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/360dialog-webhooks/references/verification.md) - Basic auth check in Node, Python and Next.js, gotchas
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/360dialog-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests