WhatsApp API

Why "sent" means nothing in the WhatsApp Cloud API

The Cloud API returns a message ID and a sent status, and your message can still never arrive. Here is why delivery is webhook-only, and what breaks when you trust the API response instead.

If you are building on the WhatsApp Cloud API, here is the single most expensive assumption you can make:

We learned this the way most teams do. A campaign went out, the logs were clean, every send returned a message ID, and the client asked why nobody was replying.

What the API actually tells you

When you POST to /messages, you get back something like this:

{
  "messaging_product": "whatsapp",
  "contacts": [{ "input": "919876543210", "wa_id": "919876543210" }],
  "messages": [{ "id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSN0YxQTdCM0Y5..." }]
}

That is an acknowledgement of receipt, nothing more. The message now sits in Meta's queue. What happens next is invisible to the response you already returned to your caller.

The status lifecycle is sent to delivered to read, and it arrives asynchronously over webhooks. Only delivered proves the message landed on a device. Meta will happily report sent and then fail the message a moment later, for a per-user cap, for a number that is not on WhatsApp, or for a quality block on your template.

Delivery is webhook-only

There is no polling endpoint. You cannot ask "did wamid X arrive?". If you did not subscribe to the messages webhook field and store what it tells you, that information is gone.

The practical shape this takes:

  1. Send. Persist the returned wamid against your outbound row immediately.
  2. Subscribe to status webhooks and write each transition into an events table, keyed by that wamid.
  3. Read delivery state from those events, never from the send response.

Step one is the one teams skip, and it is unrecoverable.

If any of your send paths (a retry job, a manual resend, an internal test hook) forgets to stamp the wamid onto the row, those messages get a permanent pair of grey ticks in your UI. Not because delivery failed, but because you have nothing to join the webhook against.

What this breaks downstream

Trusting sent does not just make your dashboard slightly wrong. Anything gated on delivery inherits the lie:

  • Flow progression. If step two fires only after step one is delivered, and your delivery status never updates, the flow stalls forever. Silently.
  • Retry logic. You cannot retry what you do not know has failed. Failures arrive as webhook failed events, with an error code attached.
  • Campaign reporting. "98% sent" is a vanity number. The only figure worth showing a client is delivered. If you are buying a tool rather than building one, ask whether its campaign report is computed from send responses or from delivery webhooks. Those are different numbers, and only one of them is true.

Not all failures are equal

When a failure does arrive, the error code decides what to do. Two matter constantly, and they pull in opposite directions:

Code Meaning Retry?
131049 Per-user marketing cap. Meta is rate-limiting this recipient to protect their experience. Yes. It is temporary. Back off and try later.
131026 The number is not on WhatsApp, or cannot receive the message. Never. It is permanent. Retrying burns quota and looks like abuse.

Treating those two the same is how you end up with a retry loop that hammers numbers which will never, under any circumstance, receive a message.

Of the two, 131049 is the one that scales with your sending. In a single month of our own production we logged 44,345 of them, and every one arrived on a message Meta had already accepted and returned an id for. The full breakdown of that error is here, and the retry-or-never table for every other code is in WhatsApp Cloud API error codes.

Watch out: Handle the code comparison in your application, not in SQL. Comparing a bound parameter against a string literal is exactly the kind of thing that works fine until it meets a database whose column collation disagrees, and then it throws at runtime inside your webhook handler, where nobody is watching.

The correction

If you take one thing from this: your outbound message table needs a wamid column, populated on every single send path, and a status that only ever moves because a webhook told it to.

Everything else, retries, flow gating, honest reporting, is downstream of getting that one join key right.

At Kredoo this is not optional plumbing. It is the difference between a follow-up system that works and one that quietly stops. If you would rather not build it yourself, that is what we do.

Questions people ask

Does a 200 response from the WhatsApp Cloud API mean the message was delivered?

No. A 200 with a message ID means Meta accepted the request for processing. The message can still fail afterwards, for a per-recipient cap, a number not on WhatsApp, or a quality block. Only a delivered status on the webhook proves it reached a device.

How do I check if a WhatsApp message was delivered?

Through the status webhook, keyed to the wamid returned by the send call. There is no polling endpoint and no way to ask Meta later whether a given message arrived. If you did not subscribe to the messages webhook field and store what it reported, that information does not exist anywhere.

What is a wamid in the WhatsApp Cloud API?

The message ID returned in the send response, in the form wamid.HBgM… It is the only key that joins a message you sent to the delivery statuses that arrive later. A message stored without its wamid can never display a delivery status, because the webhook identifies the message only by the ID you discarded.

What is the difference between sent, delivered and read on WhatsApp?

Sent means Meta accepted and queued the message. Delivered means it reached the recipient's device. Read means they opened it. The statuses arrive asynchronously over webhooks in that order, and only delivered onwards proves anything about the recipient.

Why does my WhatsApp message show one tick forever?

Usually because the outbound row was saved without its wamid, so there is nothing to join the incoming status webhook against. The delivery may well have succeeded; your system simply has no way to learn about it. Any send path that forgets to stamp the wamid produces messages whose ticks can never move.

Stop losing leads to the follow-up gap

Kredoo captures leads from Meta and Google Ads, then follows up on WhatsApp automatically. Live in 5 minutes.

Get Started