Skip to content

Handle delivery status updates

Learn how to track and process WhatsApp delivery status transitions (sent, delivered, read, failed) from webhooks.

Tracking delivery statuses allows you to monitor message deliverability, confirm customer engagement (read receipts), and detect messaging failures in real time.

Wazapin pushes these status updates to your HTTPS endpoint via the message.status_update event.


Status transition flow

Outbound messages navigate through the following statuses:

stateDiagram-v2
    [*] --> queued: API Request
    queued --> sent: Dispatched to Meta
    sent --> delivered: Arrived at Phone
    delivered --> read: Opened by User
    queued --> failed: Validation / Meta Error
    sent --> failed: Meta Delivery Error
  • queued: The message has been stored in Wazapin’s queue. (Returned immediately by the POST /v1/messages request).
  • sent: Meta/WhatsApp accepted the message.
  • delivered: The message was successfully delivered to the recipient’s phone (double checkmarks in WhatsApp).
  • read: The user opened the message (blue double checkmarks in WhatsApp).
  • failed: The message could not be sent or delivered.

Webhook payload structure

Unlike inbound message webhooks, message.status_update payloads contain the updated status directly in the webhook body. You do not need to call GET /v1/messages/{messageID} to extract the status.

Here is an example payload:

{
  "message_id": "9f1fd66d-c37a-4b50-a8c2-b4dca523f9c8",
  "conversation_id": "0f89b0f9-74b4-44f9-b9b6-48f6d4de57aa",
  "status": "delivered",
  "organization_id": "org_123"
}

Code example

Here is how to catch the webhook and process the status transitions:

# If you miss a webhook, check the status via the API:
curl -X GET "https://api.wazapin.com/v1/messages/9f1fd66d-c37a-4b50-a8c2-b4dca523f9c8/status" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Accept: application/json"
import express from "express";
import { Webhook } from "svix";

const app = express();
const wh = new Webhook(process.env.WAZAPIN_WEBHOOK_SECRET!);

app.post("/webhooks/wazapin", express.raw({ type: "application/json" }), async (req, res) => {
  try {
    wh.verify(req.body, req.headers as Record<string, string>);
  } catch (err) {
    return res.status(403).send("Invalid signature");
  }

  const payload = JSON.parse(req.body.toString("utf8"));

  // Check for status update event
  // Some payloads map event names externally, or filter per endpoint
  if (payload.status && payload.message_id) {
    const messageId = payload.message_id;
    const currentStatus = payload.status; // sent, delivered, read, failed

    console.log(`Message ${messageId} updated to: ${currentStatus}`);

    // Update your database state here...
    // db.messages.update({ where: { id: messageId }, data: { status: currentStatus } });

    if (currentStatus === "failed") {
      // Handle delivery failure (e.g. check error codes)
      console.error(`Message ${messageId} failed to deliver.`);
    }
  }

  res.status(200).send("OK");
});
from fastapi import FastAPI, Request, HTTPException
from svix.webhooks import Webhook, WebhookVerificationError
import os

app = FastAPI()
wh = Webhook(os.environ["WAZAPIN_WEBHOOK_SECRET"])

@app.post("/webhooks/wazapin")
async def handle_webhook(request: Request):
    body = await request.body()
    try:
        wh.verify(body, dict(request.headers))
    except WebhookVerificationError:
        raise HTTPException(status_code=403, detail="Invalid signature")

    payload = await request.json()
    status = payload.get("status")
    message_id = payload.get("message_id")

    if status and message_id:
        print(f"Message {message_id} status updated to: {status}")
        
        # Update your database record here
        # db.update_message_status(message_id, status)
        
        if status == "failed":
            print(f"Message {message_id} failed to deliver.")

    return {"ok": True}

Polling fallback

If your webhook receiver goes offline or you fail to receive a status update webhook, you can query the API directly as a fallback.

Wazapin provides two endpoints for status checks:

  1. GET /v1/messages/{messageID}: Retrieves the full message record including its current status.
  2. GET /v1/messages/{messageID}/status: A lightweight endpoint returning only the status metadata.

Troubleshooting

Status updates arrive out of order

Due to network concurrency, a read status webhook can occasionally arrive before a delivered webhook.

  • Always check the existing status in your database before writing an update.
  • Do not overwrite a terminal status (like read or failed) with an earlier status (like sent or delivered).

Handling failures

When a message status updates to failed, query the full message details using GET /v1/messages/{messageID}. The response will contain an error block (like error_code or failure_reason) explaining why the message could not be sent. Check the Error codes reference for details.


Navigation

Type to search…

↑↓ navigate↵ selectEsc close