Agent skill
360dialog Webhooks Skill
Receive and authenticate 360dialog WhatsApp Business API webhooks. Use when setting up a 360dialog webhook endpoint, adding HTTP Basic auth to the webhook headers, debugging 401s, or handling inbound messages, message status updates (sent, delivered, read, failed) and Coexistence events such as smb_message_echoes.
Install this skill
npx skills add hookdeck/webhook-skills --skill 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 (
messagesvsstatuses)? - 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:
{ "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:
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.
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/, examples/nextjs/, examples/fastapi/.
Event Types
Bodies are { object: "whatsapp_. 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
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
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 - Payload format, message types, statuses, Coexistence events
- references/setup.md - Setting the webhook URL and Basic auth headers with the API
- references/verification.md - Basic auth check in Node, Python and Next.js, gotchas
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests