WhatsApp API: track a message's delivery status

Last updated: 11 October 2026 · 6 min read

After sending, a message's status changes within a few seconds. You can read it with a request or receive it automatically with a webhook.

Steps

  1. 1

    Statuses

    • sent: Meta accepted it and it is on its way to the phone
    • delivered: it reached the customer's phone
    • read: the customer opened and read it
    • failed: it could not be sent; the reason is in the external_error field

    A sent that arrives right after sending does not yet mean delivered.

  2. 2

    Read the status with a request

    curl -s "https://api.clomni.ai/v1/accounts/{account_id}/whatsapp/messages/{message_id}" \
      -H "Authorization: Bearer <API_ACARI>"

    message_id is the id from the send response. The response has the same structure as the send response. You can send this request with the key of any user of the account. Checking repeatedly (polling) works, but a webhook is more efficient.

  3. 3

    Set up a webhook

    In the dashboard, on the Settings › Workspace settings › Integrations page, click Configure on the Webhook card, then Add new webhook. Enter your server's address, choose Message updated (message_updated) in the Events list and click Create webhook. The steps are described in detail in the article “Set up a webhook”.

  4. 4

    What the webhook sends

    {
      "event": "message_updated",
      "id": 918234,
      "status": "delivered",
      "message_type": "outgoing",
      "whatsapp_message_id": "wamid.HBgMOTk0NTAxMjM0NTY3...",
      "conversation": { "id": 1042 },
      "inbox": { "id": 159, "name": "Company WhatsApp" },
      "sender": { "phone_number": "+994 50 123 45 67" }
    }
    • id is the message id from the send response; store it in your own database under this id.
    • status: sent, delivered, read, failed. For a failed message the reason comes in the content_attributes.external_error field.
    • Clomni sends this request once and does not repeat it. Your server must return 2xx within 5 seconds: put the request in your own queue and answer at once.
  5. 5

    A report to your own URL for each message (status_callback)

    Add a status_callback block to the send and Clomni posts that message's status straight to your URL. Nothing to set up in the dashboard.

    {
      "channel_id": 159,
      "phone_number": "+994501234567",
      "template": { "name": "otp_code", "language": "az", "parameters": { "1": "4821" } },
      "source_reference": "otp-10422-1791700000",
      "status_callback": {
        "url": "https://example.com/whatsapp/status",
        "after_seconds": 8,
        "on_change": true,
        "secret": "<GIZLI_ACAR>"
      }
    }
    • url (required): https:// only. Private network addresses are refused and redirects are not followed.
    • after_seconds (1-3600): one report N seconds after the send, with the message's status at that moment.
    • on_change (true/false): one more report every time the status changes (delivered, read, failed). If Meta refuses the send (a wrong number, for example), failed arrives this way too.
    • At least one of after_seconds and on_change is required.
    • secret (optional): the report is signed with it. We store it encrypted and never return it.

    The request your server receives:

    {
      "event": "message_status",
      "trigger": "scheduled",
      "message_id": 918234,
      "source_reference": "otp-10422-1791700000",
      "phone_number": "+994501234567",
      "status": "delivered",
      "delivered": true,
      "read": false,
      "sent_at": "2026-10-11T08:00:01Z",
      "delivered_at": "2026-10-11T08:00:04Z",
      "read_at": null,
      "error": null,
      "after_seconds": 8,
      "checked_at": "2026-10-11T08:00:09Z"
    }
    • trigger: scheduled (the after_seconds report) or status_change (an on_change report).
    • delivered: true means two grey ticks: the message reached the phone, so the phone was online.
    • Still sent after N seconds: the phone is offline, has no internet, or WhatsApp has not been opened for a while. on_change reports the delivery when it happens.
    • read only arrives if the customer has read receipts on. Its absence does not mean the message was not read.
    • Meta usually reports a status within 1-5 seconds, so 8-15 seconds is a good after_seconds.

    Signing and retries work as for call webhooks: X-ChatX-Delivery, X-ChatX-Timestamp, X-ChatX-Signature (sha256= + HMAC-SHA256(secret, "<timestamp>.<raw body>")). Your server should answer 2xx within 5 seconds. On no connection, a timeout, 429 or 5xx we try again after 10 seconds and after 1 minute. X-ChatX-Delivery is the same on every attempt, so use it to drop repeats.

    Bulk sends take the same block: each number gets its own report, and the delay counts from that number's send. GET /messages/{message_id} also returns delivered_at, read_at and status_callback (without the secret).

  6. 6

    Get the customer's reply too

    If you also choose the Message created (message_created) event, the customer's replies and taps on template buttons reach your system too. That is exactly when the 24-hour window opens: start sending the next notifications as free text from here.

Document