How to Set Up WhatsApp API Webhooks for Live Delivery Status

·6 min read
How to Set Up WhatsApp API Webhooks for Live Delivery Status

A customer in Lahore orders a leather jacket online. Your system automatically fires a WhatsApp message: "Your order is dispatched." Your backend database marks it as sent. But did the customer actually get it? Is their phone switched off because of a power outage? Did they block your business number? Or are they actively reading your message right now, preparing the Cash on Delivery (COD) amount?

If you only rely on the API response from your HTTP POST request, you are operating in the dark. That initial response only tells you that Meta accepted your message. It does not mean the message reached the handset. To know if a message was delivered or read, you must set up a webhook. This is a permanent, secure listener on your server that Meta calls every time a message status changes.

Your Two Real Options for Webhook Integration

You have two main paths to get these real-time status updates. You can go direct to Meta, or you can use an intermediary wrapper API.

FeatureDirect Meta Cloud APIBSP / Wrapper API (like WA Link)
Setup ComplexityHigh. Requires Meta Developer App setup, SSL management, and raw JSON parsing.Low. Cleaned up payloads, easier dashboard interface.
Hosting CostFree from Meta, but you pay for your own secure server (AWS, DigitalOcean).Subscription fee, but often includes managed routing and retries.
Debugging ToolsMeta App Dashboard (sometimes slow to update debug logs).Real-time request logs on the provider's dashboard.
Payload FormatDeeply nested JSON with complex arrays.Flattened, simplified JSON keys.

Going direct to Meta is free in terms of software licensing, but it demands engineering hours. You must configure your own secure endpoint, handle verification handshakes, and parse complex, nested JSON objects.

If you use WA Link, we handle the complex handshake and payload normalization for you. We strip out the unnecessary metadata and send a clean, flat payload to your server. However, we do not store your customer database, so you still need your own backend to update your order statuses based on these pings.

Step 1: Build the Verification Endpoint (GET Request)

Before Meta sends you status updates, it wants to verify that you actually own the URL you registered. It does this by sending a one-time GET request to your server. Your server must read the query parameters, verify a token you created, and return a specific challenge string in plain text.

If your server returns HTML, a JSON object, or a 404 error, Meta will reject your webhook URL. Here is how to handle this verification step using Node.js and Express:

const express = require('express');
const app = express();
app.use(express.json());

// This token is a secret string you define in your Meta Developer Portal
const VERIFY_TOKEN = "MySecureToken123!";

app.get('/webhook', (req, res) => {
    const mode = req.query['hub.mode'];
    const token = req.query['hub.verify_token'];
    const challenge = req.query['hub.challenge'];

    if (mode && token) {
        if (mode === 'subscribe' && token === VERIFY_TOKEN) {
            console.log('Webhook verified successfully.');
            return res.status(200).send(challenge);
        } else {
            return res.sendStatus(403);
        }
    }
    return res.sendStatus(400);
});

Upload this code to your live server. It must have an active SSL certificate (HTTPS). Meta will not send webhooks to an insecure http:// URL. If you are developing locally in Mumbai or Karachi, use a tool like Cloudflare Tunnels or Ngrok to expose your local port via HTTPS for testing.

Step 2: Handle the Status Payloads (POST Request)

Once verified, Meta will start sending POST requests to that same /webhook URL whenever a message is sent, delivered, read, or failed. This payload is nested deeply. You need to drill down into the entry, changes, value, and statuses arrays.

Here is a typical JSON payload sent by Meta when a message is successfully delivered to a user's phone:

{
  "object": "whatsapp_business_account",
  "entry": [
    {
      "id": "10982374928374",
      "changes": [
        {
          "value": {
            "messaging_product": "whatsapp",
            "metadata": {
              "display_phone_number": "923001234567",
              "phone_number_id": "9876543210"
            },
            "statuses": [
              {
                "id": "wamid.HBgLOTIzMDAxMjM0NTY3FQIAERgSM0EzQjRDNUU2RjdCOEE5OUIzAA==",
                "status": "delivered",
                "timestamp": "1711964400",
                "recipient_id": "923219876543"
              }
            ]
          },
          "field": "messages"
        }
      ]
    }
  ]
}

Note the structure of the statuses array. It contains the unique message ID (id), the status (which can be sent, delivered, read, or failed), the Unix timestamp, and the recipient's phone number. Your code must parse this array and update your local database.

app.post('/webhook', (req, res) => {
    const body = req.body;

    if (body.object === 'whatsapp_business_account') {
        if (body.entry && body.entry[0].changes && body.entry[0].changes[0].value.statuses) {
            const statusData = body.entry[0].changes[0].value.statuses[0];
            const messageId = statusData.id;
            const status = statusData.status; // sent, delivered, read, failed
            const recipient = statusData.recipient_id;

            console.log(`Message ${messageId} to ${recipient} is now: ${status}`);

            // Update your database here (e.g., SQL UPDATE orders SET delivery_status = status WHERE msg_id = messageId)
        }
        
        // Always return a 200 OK fast to prevent Meta from retrying
        return res.status(200).send('EVENT_RECEIVED');
    } else {
        return res.sendStatus(404);
    }
});

Critical Mistakes You Must Avoid

Setting up the code is simple, but running it at scale in production reveals several architectural traps that can crash your server or get your webhook disabled by Meta.

1. Processing Data Before Replying

Meta expects a 200 OK response within 3 seconds. If your server takes longer—perhaps because you are running a slow database query or waiting on a third-party CRM API—Meta assumes the delivery failed. It will retry.

This creates a backlog. Your server gets hit with duplicate requests, database connections pool up, and eventually, your server crashes. Always send the 200 OK response immediately, then process the webhook asynchronously using a message queue like RabbitMQ, BullMQ, or a simple background worker.

2. Ignoring Out-of-Order Webhooks

Mobile networks are unpredictable. A user might be traveling through a poor coverage area in rural Sindh or Bihar. Their phone might receive the message, send a "delivered" status, and immediately send a "read" status when they open it.

Due to network routing, your server might receive the "read" webhook *before* the "delivered" webhook. If your backend logic blindly overwrites the status based on whatever arrived last, you might downgrade a "read" message back to "delivered". Always compare the timestamp field in the webhook payload. Only update the status in your database if the incoming timestamp is newer than the stored timestamp.

3. Neglecting Webhook Security

If your webhook URL is public, anyone can send fake POST requests to it, marking unpaid orders as paid or spamming your logs. You must verify that the incoming request actually came from Meta.

Meta signs every webhook payload with an X-Hub-Signature-256 header. This signature is an HMAC-SHA256 hash of the raw request body, signed with your Meta App Secret. If you do not verify this signature on every incoming POST request, your system is vulnerable. You can read the official validation steps on the Meta Webhooks Security Guide.

Frequently Asked Questions

Why does Meta keep disabling my webhook URL automatically?

If your server returns error codes (like 500, 502, or 404) or takes longer than 3 seconds to respond to more than 10% of requests over a sustained period, Meta will automatically pause or disable your subscription. Monitor your server's error logs and ensure you are returning 200 OK immediately before doing any heavy processing.

How do I handle error codes when a message status is "failed"?

When a status is failed, the payload includes an errors array inside the status object. It contains an error code and a title. For example, error code 1310429 means you have hit your daily messaging limit, while code 131026 means the recipient's phone number is not registered on WhatsApp. Log these codes to keep your customer database clean.

Can I use one webhook URL for multiple WhatsApp business numbers?

Yes. If you manage multiple numbers under the same Meta Business Manager, the incoming webhook payload will specify the business phone number ID in the metadata.phone_number_id field. Use this ID to route the status update to the correct business account in your database.

Do I get charged for webhook delivery status notifications?

Meta does not charge you for sending webhook payloads to your server. However, you must pay for your own hosting, bandwidth, and database writes to process them. If you send millions of messages monthly, the database write volume can become a significant infrastructure cost.

Read the API documentation