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 (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:

{ "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_business_account", entry: [{ id, changes: [{ field, value }] }] }. Dispatch on entry[].changes[].field, and within messages on which array is present:

fieldPresent in valueMeaning
messagesmessagesInbound message; messages[].type is text, image, video, audio, document, sticker, location, contacts, reaction, interactive, button, order, system, unsupported, ...
messagesstatusesOutbound message status: sent, delivered, read, failed, played
messageserrorsOperational or delivery errors outside the normal message flow
smb_message_echoesmessage_echoesCoexistence: messages the business sent from the WhatsApp Business app
historyhistoryCoexistence: chat history shared after onboarding
smb_app_state_syncstate_syncCoexistence: 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 4, 2026

View on GitHub →