# Guide to 360dialog Webhooks: Features and Best Practices

360dialog webhooks carry everything that happens on your WhatsApp Business numbers: inbound messages from customers, delivery and read receipts for the messages you send, and errors. 360dialog is a WhatsApp Business Solution Provider, and it forwards the WhatsApp Business Platform's notifications to a URL you choose. Its docs describe webhooks as "core infrastructure, not optional": without a working webhook URL you can send WhatsApp messages but you never receive replies.

This guide covers how 360dialog webhooks are configured and authenticated, the shape of message and status notifications, the strict delivery rules, and the best practices for running a 360dialog receiver at WhatsApp volumes.

## What are 360dialog webhooks?

When you set a webhook URL in 360dialog, the WhatsApp Business Platform sends a JSON notification to it whenever an event occurs. The bodies follow Meta's WhatsApp Cloud API format. 360dialog's docs name three main objects inside them:

* **messages**: a new inbound message, including its content.
* **statuses**: a change in the status of a message you sent.
* **errors**: operational or delivery problems outside the normal message flow.

You can set the URL at two levels. A phone number webhook receives events for that number only, and is the recommended setup. A WABA webhook (for a WhatsApp Business Account) receives events for every number in the account that has no webhook of its own.

360dialog's webhook docs don't describe a signature. The authentication they document is a header you attach when you set the URL: `Authorization: Basic base64(USER:PASS)`. 360dialog sends that header with every notification.

## 360dialog webhook features

| Feature | Details |
| --- | --- |
| Configuration | 360dialog Hub (**API Settings** on the number), or the Messaging API on `waba-v2.360dialog.io` |
| Levels | Phone number webhook (preferred) or WABA webhook (fallback) |
| URL rules | HTTPS with a valid certificate. No underscores in the domain and no explicit port |
| Body | Meta's Cloud API notification: `object`, then `entry[].changes[]` with `field` and `value` |
| Event identifier | `entry[].changes[].field`, then which array is present in `value` |
| Authentication | Custom `headers` set with the URL, documented as `Authorization: Basic base64(USER:PASS)`. No signature |
| Success response | HTTP `200` within 5 seconds |
| Retries | Exponential backoff. The webhook page says up to 7 days; the reference page says typically 24 hours |
| Volume | About three status callbacks per outbound message, plus one per inbound message |
| Source IPs | Not published |

## Common events

The `field` on each change says what kind of notification it is. Nearly all traffic is `messages`, and within it you dispatch on which array `value` contains.

| `field` | In `value` | Meaning |
| --- | --- | --- |
| `messages` | `messages[]` | An inbound message. `messages[].type` says whether it's text, an image, a reaction, an interactive reply and so on |
| `messages` | `statuses[]` | A status change for a message you sent: `sent`, `delivered`, `read`, `failed` or `played` |
| `messages` | `errors[]` | An error outside the normal message flow |
| `smb_message_echoes` | `message_echoes` | WhatsApp Coexistence: a message the business sent from the WhatsApp Business app |

Two more Coexistence events, `history` and `smb_app_state_sync`, use a different body of `{ id, event, data }`, with the type in the top-level `event` field.

The status values map to what users see in WhatsApp: `sent` is one checkmark, `delivered` two, `read` two blue checkmarks, `failed` a red error triangle, and `played` means a voice message was played for the first time.

Here is 360dialog's example of a failed status, trimmed:

```json
{
  "object": "whatsapp_business_account",
  "entry": [
    {
      "id": "102290129340398",
      "changes": [
        {
          "field": "messages",
          "value": {
            "messaging_product": "whatsapp",
            "metadata": {
              "display_phone_number": "15550783881",
              "phone_number_id": "106540352242922"
            },
            "statuses": [
              {
                "id": "wamid.HBgLMTIxMTU1NTc5NDcVAgARGBIyRkQxREUxRDJFQUJGMkQ3NDIA",
                "status": "failed",
                "timestamp": "1689380458",
                "recipient_id": "15551234567",
                "errors": [
                  {
                    "code": 130472,
                    "title": "User's number is part of an experiment"
                  }
                ]
              }
            ]
          }
        }
      ]
    }
  ]
}

```

`timestamp` is a string of Unix seconds, and `entry` and `changes` are arrays, so loop over both rather than reading index `0`. `metadata.phone_number_id` tells you which of your numbers the event belongs to, which matters on a WABA webhook.

360dialog's pages list only the fields above. Meta's Cloud API defines other webhook fields, such as template status updates, and 360dialog doesn't say whether it forwards them, so acknowledge any `field` you don't recognise.

## Setting up 360dialog webhooks

1. Choose a username and a long random password, for example with `openssl rand -hex 32`, and base64-encode them as `USER:PASS`:
  
  
  ```bash
  printf '%s' "$USER:$PASS" | base64
  
  ```
  
  
2. Set the webhook URL with an `Authorization` header in its `headers` object. In the 360dialog Hub, select the number, open **API Settings**, edit the number's webhook or the WABA webhook, enter the endpoint and the header, save, and confirm the verification code 360dialog emails you. Through the API, send the URL and headers with your `D360-API-KEY`:
  
  
  ```bash
  curl -X POST "https://waba-v2.360dialog.io/v1/configs/webhook" \
    -H "D360-API-KEY: $D360_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://example.com/webhooks/360dialog",
      "headers": { "Authorization": "Basic dGVzdHVzZXI6dGVzdHBhc3M=" }
    }'
  
  ```
  
  
  
  The WABA-level equivalent is `POST /waba_webhook`. The API also has a multi-webhook feature whose named destinations each take their own `url` and `headers`.
3. **In Hookdeck.** Create a source with the 360dialog type and enter the same username and password. Use the source URL as the webhook URL. Hookdeck reads the `Authorization: Basic` header and compares both values.

Because the credentials are a header 360dialog sends on every request, they arrive on the first attempt. Hookdeck never sends a `401` challenge, and doesn't need to.

## Securing 360dialog webhooks

Decode the `Authorization` header, split it on the first colon, and compare the username and password with your stored values in constant time.

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

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

function verifyBasicAuth(header, user, pass) {
  if (!user || !pass) return false; // fail closed when unconfigured
  const match = /^basic\s+(\S+)\s*$/i.exec(header || '');
  if (!match) return false;
  const decoded = Buffer.from(match[1], 'base64').toString('utf8');
  const i = decoded.indexOf(':'); // passwords may contain ':', usernames may not
  if (i === -1) return false;
  const userOk = safeEqual(decoded.slice(0, i), user);
  const passOk = safeEqual(decoded.slice(i + 1), pass);
  return userOk && passOk;
}

const app = express();

app.post('/webhooks/360dialog', express.raw({ type: '*/*' }), (req, res) => {
  const ok = verifyBasicAuth(
    req.get('authorization'),
    process.env.THREE_SIXTY_DIALOG_WEBHOOK_USERNAME,
    process.env.THREE_SIXTY_DIALOG_WEBHOOK_PASSWORD
  );
  if (!ok) return res.sendStatus(401);

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

  // Acknowledge inside 5 seconds, then process.
  res.sendStatus(200);

  for (const entry of body.entry ?? []) {
    for (const change of entry.changes ?? []) {
      // queue change.field and change.value for a worker
    }
  }
});

```

The same check in Python:

```python
import base64
import binascii
import hmac
import os
import re

def verify_basic_auth(header, user, password) -> bool:
    if not user or not password:
        return False  # fail closed
    match = re.fullmatch(r"(?i)basic\s+(\S+)\s*", header or "")
    if not match:
        return False
    try:
        decoded = base64.b64decode(match.group(1), validate=True)
    except (binascii.Error, ValueError):
        return False
    received_user, sep, received_pass = decoded.partition(b":")
    if not sep:
        return False
    user_ok = hmac.compare_digest(received_user, user.encode("utf-8"))
    pass_ok = hmac.compare_digest(received_pass, password.encode("utf-8"))
    return user_ok and pass_ok

# verify_basic_auth(request.headers.get("authorization"),
#                   os.environ["THREE_SIXTY_DIALOG_WEBHOOK_USERNAME"],
#                   os.environ["THREE_SIXTY_DIALOG_WEBHOOK_PASSWORD"])

```

Basic credentials prove the sender knows them. They don't prove the body is unchanged, and a captured request can be sent again and still pass, because nothing is signed and no timestamp is sent. Serve the endpoint over HTTPS only, use a long random password, and rotate it by setting the webhook URL again with new headers if it may have leaked.

> **Make 360dialog webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the Basic credentials on every notification, answers inside 360dialog's 5-second window, and queues each event durably for your service.

## 360dialog webhook limitations and pain points

### Five seconds to answer

**The Problem:** A handler that does real work before responding sees duplicates, a growing retry queue and delayed messages.

**Why It Happens:** A notification only counts as delivered if your URL returns `HTTP 200 OK` within 5 seconds. Any other response, or a slow one, puts it in a retry queue. 360dialog warns that "poor webhook performance can therefore delay message processing", and its own guidance is to acknowledge immediately and process afterwards.

**Workarounds:**

* Return `200` straight after the credential check and process the notification in a background job.
* Aim for 360dialog's latency targets: a median under 250 ms and fewer than 1% of requests over one second.

**How Hookdeck Can Help:** Hookdeck acknowledges each notification as it arrives and stores it, then delivers it to your service at a pace it can handle, with retries you control. Slow processing on your side no longer counts as a failed WhatsApp delivery.

### Status callbacks outnumber messages

**The Problem:** Traffic is several times what you expected from your message volume, and spikes during campaigns.

**Why It Happens:** Each outbound message typically produces `sent`, `delivered` and `read` callbacks. 360dialog's sizing rule is capacity for three times your outbound volume as status callbacks, plus your inbound volume. Its example: sending 1,000 messages per second with a 30% reply rate means up to 3,000 status webhooks and about 300 inbound message webhooks per second.

**Workarounds:**

* Size your receiver from 360dialog's rule, not from your inbound traffic.
* Store only the newest status per message ID and drop older ones.

**How Hookdeck Can Help:** Hookdeck absorbs bursts at the edge and can rate-limit delivery to your service. [Filters](/docs/filters) can send status callbacks and inbound messages to different destinations, so a campaign's receipts never slow down customer replies.

### A static credential, not a signature

**The Problem:** Anyone who learns the Basic credentials can post fake inbound messages or statuses, and nothing detects a changed body.

**Why It Happens:** 360dialog's documented authentication is a header you set once. Its webhook pages don't describe a signature, and nothing in the request is computed over the body.

**Workarounds:**

* Use HTTPS only and a long random password, and keep the encoded header out of logs.
* Deduplicate on the message ID (`messages[].id` or `statuses[].id`), so a replayed notification has no further effect.

**How Hookdeck Can Help:** Hookdeck's 360dialog source type checks the Basic username and password on every request. Hookdeck answers 360dialog with a `200` either way, then records a request with missing or wrong credentials as failed verification, so it never reaches your destination.

### Unclear retry window

**The Problem:** You can't say how long an outage you can survive before notifications are lost.

**Why It Happens:** 360dialog's webhook page says Meta retries failed webhooks "for up to 7 days" with exponential backoff. Its webhook reference says retries continue for "typically 24 hours, though this may vary".

**Workarounds:**

* Plan for the shorter figure, 24 hours.
* After a long outage, reconcile message state rather than assuming every status will arrive.

**How Hookdeck Can Help:** Once a notification has reached Hookdeck it's stored and can be replayed whenever your service is ready, so the length of your outage no longer depends on Meta's retry window.

## Best practices

### Dispatch on `field`, then on the array present

Loop over `entry[]` and `changes[]`. For `field` `messages`, check for `value.messages`, `value.statuses` or `value.errors`. Handle the Coexistence shapes separately, and acknowledge anything you don't recognise.

### Deduplicate on message IDs

Retries resend the same notification. Use the `wamid` in `messages[].id` or `statuses[].id`, together with the status value, as your idempotency key. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Don't assume order

Statuses can arrive out of order or late. Keep a rank for each status (`sent` < `delivered` < `read`) and never move a message backwards. `failed` should always win.

### Acknowledge fast and process asynchronously

This is the rule 360dialog stresses most. Verify, persist, return `200`, then process. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Prefer phone number webhooks

A phone number webhook only receives that number's events, which keeps routing simple and limits the impact of a bad deploy. Use a WABA webhook only as a fallback, and read `metadata.phone_number_id` if you do.

## Conclusion

360dialog forwards WhatsApp Business Platform notifications in Meta's Cloud API format, protected only by the `Authorization: Basic` header you set with the URL. Check those credentials in constant time over HTTPS, dispatch on `field` and the array present, deduplicate on message IDs, and answer within 5 seconds, because the status traffic alone can be three times your send volume.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks the Basic credentials, answers 360dialog immediately, and holds every notification until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle 360dialog webhooks reliably in minutes.