How to Get Reliable WhatsApp API Delivery Reports for Every Message

When you send a critical notification to a customer, a successful HTTP status code 200 from the Meta API does not mean your message was delivered. It only means Meta accepted your payload. If your customer’s phone is switched off because of a power outage in Karachi, or if they are traveling through a low-coverage area in Bihar, that message sits undelivered on Meta's servers.
To know if your message actually reached the screen, you must implement delivery reports. Relying solely on the initial API response leaves you blind. This guide walks you through setting up a production-ready webhook system to track the lifecycle of every single WhatsApp message you send.
Why the HTTP Send Response Lies to You
When you make a POST request to send a message, Meta responds immediately with a unique message ID. This response is synchronous. Meta cannot tell you at that exact millisecond whether the recipient’s phone is online, out of balance, or has blocked your number.
The actual lifecycle of a message happens asynchronously. It transitions through four distinct statuses: sent, delivered, read, and failed. To capture these state changes, you must set up an active listener on your server. This listener is a webhook endpoint that Meta calls every time a message status updates. If you do not build this, you are sending messages into a black hole.
For a deep dive into how these statuses impact your billing and API performance, read our guide on Tracking WhatsApp API Message Statuses: Webhooks, Error Codes, and Costs.
What You Need Before You Start
Do not write any code until you have these four components ready:
- A Meta Developer Account: Your app must be configured with the WhatsApp product active.
- A Verified WhatsApp Business Account (WABA): You need a verified phone number associated with your WABA.
- An HTTPS Server Endpoint: Meta will not send webhook payloads to an insecure HTTP URL. For local development, you cannot use localhost directly. Use a tunneling tool like Ngrok or LocalTunnel to expose your local development port (e.g., port 8000) to a secure public URL.
- A Relational Database: You need a table to store message IDs and correlate them with incoming status updates.
Step 1: Preparing Your Database Schema
Before you configure webhooks, your database must be ready to handle the incoming data. When you send a message, you must store its unique Meta message ID. When the webhook fires, you will use this ID to update the record.
Create a table in your database with the following structure. This SQL example uses standard relational design:
| Column Name | Data Type | Description |
|---|---|---|
| id | BIGINT AUTO_INCREMENT PRIMARY KEY | Your internal database primary key. |
| recipient_phone | VARCHAR(20) | The phone number in international format (e.g., 923001234567). |
| whatsapp_message_id | VARCHAR(255) UNIQUE KEY | The unique ID returned by Meta when you send the message. Index this column. |
| delivery_status | VARCHAR(50) DEFAULT 'pending' | Tracks states: pending, sent, delivered, read, failed. |
| delivered_at | TIMESTAMP NULL | The exact time the user's phone received the message. |
| read_at | TIMESTAMP NULL | The exact time the user opened the message. |
| error_code | INT NULL | Meta error code if the message fails to deliver. |
Crucial performance warning: You must place a unique index on the whatsapp_message_id column. If you do not, your database will perform a full table scan every time a webhook arrives. When you scale your messaging volume, your database CPU usage will spike to 100%, causing your entire application to hang.
Step 2: Building the Webhook Verification Endpoint
Meta requires a two-step verification process for your webhook. When you first register your URL in the Meta Developer Portal, Meta sends a GET request to your server to verify that you own the domain. Once verified, Meta will only send POST requests containing the actual message status payloads.
Your GET endpoint must look for three query parameters: hub.mode, hub.verify_token, and hub.challenge.
Your server code must perform these exact steps:
- Check if the
hub.modeis set to the string "subscribe". - Check if the
hub.verify_tokenmatches a secret, random string that you define (e.g., "my_secret_token_123"). - If both match, return the exact string value of
hub.challengeas plain text with an HTTP status code of 200.
If you return the challenge wrapped in JSON, or if you return HTML tags, Meta's validation tool will fail, and you will not be able to save your webhook configuration.
Step 3: Configuring the Webhook in the Meta Developer Console
Once your verification endpoint is live (or exposed via Ngrok), configure it in your Meta dashboard:
- Go to the Meta Developer Portal and select your application.
- In the left-hand menu, expand "WhatsApp" and click on "Configuration".
- In the "Webhooks" section, click "Edit".
- Paste your secure webhook URL (e.g.,
https://yourdomain.com/webhooks/whatsapp). - Enter the exact Verification Token you defined in your code.
- Click "Verify and Save". Meta will make a GET request to your server immediately. If successful, the modal will close.
- Now, click "Manage" under Webhooks fields.
- Locate the row named messages and click "Subscribe". If you skip this step, you will not receive any delivery reports.
Step 4: Parsing the Delivery Report Payload
Once subscribed, Meta will send a POST request to your URL every time a status changes. The payload is a deeply nested JSON object. Below is the structure of the incoming data for a successful delivery event:
| JSON Path | Expected Value | Action Required |
|---|---|---|
| entry[0].changes[0].value.statuses[0].id | wamid.HBgMOTIz... | Extract this ID to search your database table. |
| entry[0].changes[0].value.statuses[0].status | delivered | Update your delivery_status column to this value. |
| entry[0].changes[0].value.statuses[0].timestamp | 1710923456 | Convert this UNIX timestamp and save it to delivered_at. |
| entry[0].changes[0].value.statuses[0].errors[0].code | Integer (e.g., 131042) | If status is "failed", log this error code to debug delivery issues. |
When parsing this payload, always write defensive code. Ensure your script checks if the statuses array exists in the payload before trying to read index 0. If a customer sends you an incoming text message, Meta uses the exact same webhook endpoint but sends a messages array instead of a statuses array. If your code assumes statuses[0] is always present, your script will throw a fatal error and return an HTTP 500 to Meta.
How to Confirm Your Webhook Integration Works
Do not assume your system works because the verification step passed. Run this controlled manual test:
- Insert a dummy record into your database with a status of "pending".
- Send a WhatsApp template message to your personal phone number.
- Capture the message ID returned in the JSON response from Meta's send API. Update your dummy database record with this ID.
- Turn off mobile data and Wi-Fi on your personal phone.
- Check your database. You should see a webhook update the status to "sent".
- Turn on your phone's mobile data. Do not open the WhatsApp app yet.
- Check your database. The status must update to "delivered" with the current timestamp.
- Open the WhatsApp app and read the message.
- Check your database. The status must update to "read".
What to Do When Webhooks Fail
If you are not seeing status updates in your database, check for these common failure points:
The Challenge Verification Fails
If Meta says your server did not return the challenge, check your server logs. Ensure your framework is not appending middleware that alters the response. For example, some frameworks automatically convert string responses into JSON objects, or append CSRF tokens. Your endpoint must return raw text.
Webhooks Stop Arriving After a Few Hours
Meta monitors your webhook's response time and error rate. If your server takes longer than 3 seconds to respond, or if you return HTTP 500 errors, Meta will queue and retry the webhooks. If your server remains unhealthy, Meta will eventually pause your webhook subscription. Always return an HTTP 200 response immediately, before performing heavy database processing.
To handle high-volume traffic without crashing your database, you must decouple webhook reception from database writing. Use a queue worker system. When a webhook arrives, push the raw JSON payload onto a queue (like Redis or RabbitMQ) and immediately return an HTTP 200 to Meta. Let a background worker process the queue and update the database at its own pace. For architectural strategies on handling high-volume messaging, read about Why Your WhatsApp API Calls Are Failing: Rate Limits, Queues, and Production-Ready Architecture.
Next Steps for Your Messaging System
Once your delivery reports are working reliably, you can build automation around them