---
title: "Handle delivery status updates"
description: "Learn how to track and process WhatsApp delivery status transitions (sent, delivered, read, failed) from webhooks."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.wazapin.id/llms.txt
> Use this file to discover all available pages before exploring further.

# Handle delivery status updates

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:

```mermaid
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:

```json message.status_update webhook body
{
  "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:

### cURL

```bash
# 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"
```
### TypeScript

```typescript
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");
});
```
### Python

```python
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](/api/errors) for details.

---

## Related links
- [Message lifecycle concepts](/whatsapp-basics/message-lifecycle)
- [Webhooks overview](/receive-messages/overview)
- [GET /v1/messages/\{messageID\} status reference](/api-reference)

Source: https://docs.wazapin.id/guides/handle-delivery-status/index.mdx
