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

# Delivery Status Event

> message_status_updated — follow a message to delivered and read

## Event Overview

**Event Name:** `message_status_updated`

**Trigger:** WhatsApp reports a new delivery state for a message you sent.

**Method:** `POST`\
**Content-Type:** `application/json`

<Warning>
  **This is the highest-volume event of the set.** One outbound message normally produces three deliveries — `sent`, then `delivered`, then `read`. On a busy account that is several times your message traffic. Enable it only if you actually track delivery.
</Warning>

## Payload Structure

```json theme={null}
{
  "event": "message_status_updated",
  "data": {
    "userId": "6803fa770b666a64ab1694c1e",
    "agentId": "7103fa770b666a64ab1694c1e",
    "channel": "whatsapp",
    "message_id": "9f2c1b7e5a4d3f8c6b0e2a13",
    "msg_id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAEBgg",
    "type": "template",
    "template": "order_shipped",
    "from": "919000000000",
    "to": "919876543210",
    "contact_number": "919876543210",
    "status": "delivered",
    "previous_status": "sent",
    "agent_phone_number_id": "123456789012345",
    "wa_campaign_id": "spring_sale_2026",
    "timestamp": "1772345678"
  }
}
```

## Field Descriptions

| Field                   | Type   | Description                                                                                                                                                                     |
| ----------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `userId`                | String | Your CallKaro account ID                                                                                                                                                        |
| `agentId`               | String | The chat agent that owns the conversation                                                                                                                                       |
| `channel`               | String | `whatsapp`                                                                                                                                                                      |
| `message_id`            | String | CallKaro's ID for the message — the same value the original `message_sent` / `template_sent` carried                                                                            |
| `msg_id`                | String | WhatsApp's `wamid`                                                                                                                                                              |
| `type`                  | String | The original message's type                                                                                                                                                     |
| `template`              | String | Template name, if it was a template                                                                                                                                             |
| `from`                  | String | Your number                                                                                                                                                                     |
| `to`                    | String | The customer's number                                                                                                                                                           |
| `contact_number`        | String | The customer — same as `to`, since only outbound messages get status updates                                                                                                    |
| `status`                | String | The **new** status                                                                                                                                                              |
| `previous_status`       | String | What it was before this update, so you can detect the transition rather than re-deriving it                                                                                     |
| `agent_phone_number_id` | String | The WhatsApp `phone_number_id`                                                                                                                                                  |
| `wa_campaign_id`        | String | Campaign tag from the original send                                                                                                                                             |
| `timestamp`             | String | WhatsApp's own timestamp for the status change — a **Unix epoch second string**, not the `YYYY-MM-DD HH:MM:SS` format the other events use. May be empty if WhatsApp omitted it |

<Tip>
  Join on `message_id` (or `msg_id`) to match a status update back to the send you already recorded.
</Tip>

## Status values

| Status      | Reached the user? | Meaning                                                  |
| ----------- | ----------------- | -------------------------------------------------------- |
| `sent`      | **Not yet**       | WhatsApp accepted it, but it is not on the user's device |
| `delivered` | **Yes**           | On their device, not yet opened                          |
| `read`      | **Yes**           | Opened                                                   |
| `failed`    | **No**            | WhatsApp could not deliver it                            |

<Note>
  Statuses are not guaranteed to be strictly ordered or complete. A user with read receipts off never produces `read`; a phone that is off stays at `sent` indefinitely. Treat these as "the furthest state so far" rather than a sequence you must see every step of.
</Note>

## Example Implementation

### Node.js (Express)

```javascript theme={null}
app.post('/webhook/callkaro', (req, res) => {
  res.sendStatus(200); // acknowledge first

  const { event, data } = req.body;
  if (event !== 'message_status_updated') return;

  // previous_status makes the transition explicit -- no need to keep
  // your own state machine to know what just changed.
  console.log(
    `${data.msg_id}: ${data.previous_status} -> ${data.status}`
  );

  if (data.status === 'failed') {
    alertOps(`Delivery failed to ${data.contact_number}`);
  }

  if (data.status === 'read' && data.wa_campaign_id) {
    trackCampaignRead(data.wa_campaign_id, data.contact_number);
  }
});
```

### Python (FastAPI)

```python theme={null}
from datetime import datetime, timezone

REACHED_USER = {"delivered", "read"}


@app.post("/webhook/callkaro")
async def handle_webhook(request: Request):
    payload = await request.json()
    if payload.get("event") != "message_status_updated":
        return {"status": "success"}

    data = payload.get("data", {})

    # NOTE: unlike the other events, this timestamp is Unix epoch seconds.
    raw = data.get("timestamp") or "0"
    changed_at = datetime.fromtimestamp(int(raw), tz=timezone.utc)

    print(
        data.get("msg_id"),
        data.get("previous_status"), "->", data.get("status"),
        "at", changed_at.isoformat(),
    )

    if data.get("status") in REACHED_USER:
        mark_delivered(data.get("message_id"))
    elif data.get("status") == "failed":
        mark_failed(data.get("message_id"), data.get("contact_number"))

    return {"status": "success"}
```

## Related

<CardGroup cols={2}>
  <Card title="Message Events" icon="message" href="/webhook/message-events">
    The sends these updates refer to
  </Card>

  <Card title="Send WhatsApp Template" icon="paper-plane" href="/api-reference/whatsapp-send-template">
    A `200` there does not mean the user got it
  </Card>
</CardGroup>
