DEV Community

Eliran Godov
Eliran Godov

Posted on

Why a Successful WhatsApp API Request Does Not Mean the Message Was Delivered

If you've built any kind of WhatsApp automation, you've probably had this moment: your code calls the WhatsApp Business API, gets back an HTTP 200 and a message ID, logs "message sent," and moves on. Then, hours later, someone tells you the customer never actually received anything.

Nothing crashed. No exception was thrown. Every log line said success. And the message still didn't arrive.

This isn't a bug in your code. It's a gap between what the API response actually promises and what most of us assume it promises. If you're building anything that talks to customers over WhatsApp - a support bot, a lead-notification flow, an order-confirmation system - this distinction is worth understanding before it costs you a real conversation.

Accepted, sent, delivered, read: four different things

The WhatsApp Business API (Cloud API or on-prem) doesn't have a single "message status." It has a sequence of them, and each one means something narrower than people assume:

  • Accepted - the API validated your request and queued the message. This is what your HTTP 200 response actually confirms. It says "I understood you and I'm going to try." It does not say the message reached WhatsApp's servers for onward delivery, and it definitely doesn't say it reached the customer's phone.
  • Sent - the message left WhatsApp's infrastructure toward the recipient's device.
  • Delivered - the recipient's device confirmed receipt.
  • Read - the recipient opened the conversation (assuming read receipts aren't disabled on their end, which is common and outside your control).

Each of these is a distinct event, delivered asynchronously, after the original API call has already returned. The HTTP response to your send request only ever tells you about the first one. Treating "accepted" as "delivered" is the single most common mistake in WhatsApp automation code, because it's an easy mistake to make - the request looks successful in every way your code checks.

Why HTTP 200 isn't proof of delivery

This matters because a 200 status code, in most API integrations we build, is the end of the story: the write succeeded, the email queued, the record was saved. WhatsApp doesn't work that way, and it can't - the API is handing your message to a separate delivery pipeline it doesn't fully control (the recipient's device has to be reachable, has to accept the message, and the whole path runs over infrastructure outside WhatsApp's servers).

So a 200 response is really: "I've accepted responsibility for trying." Anything after that - sent, delivered, read, or failed - is a separate, later fact your system has to go find out about. If your monitoring only checks the response to the send call, you have zero visibility into whether the message actually reached anyone.

Webhooks are the real source of truth

The only way to know what actually happened to a message is to listen for delivery status webhooks. WhatsApp sends status update events (sent, delivered, read, failed) to a webhook endpoint you configure, keyed to the message ID you got back from the original send call.

This has a practical implication that's easy to miss when you're first building: your system needs to persist that message ID and treat the record as "pending" until a webhook resolves it - not "sent" and forgotten. If you don't have webhook handling wired up and correctly matched back to your outbound messages, you don't have delivery visibility at all, regardless of what your API responses say.

It also means delivery status is eventually consistent, not synchronous. A workflow that needs to know a message was delivered before doing the next step has to wait for that webhook, not for the original API response.

The 24-hour customer service window

A separate mechanic compounds this: WhatsApp's 24-hour customer service window. Once a customer messages you, you can send free-form replies for 24 hours from their last message. After that window closes, free-form messages are rejected - you can only continue the conversation using a pre-approved message template.

This intersects with the delivery-status problem in an ugly way. If your system doesn't track window state and tries to send a free-form message after it's closed, that failure shows up as a rejected or failed message - sometimes with an "accepted" response if the failure surfaces asynchronously, sometimes as a synchronous error, depending on exactly how it's caught. Either way, if you're not specifically watching for it, a closed window looks a lot like a generic delivery failure, and it's easy to lose the actual cause in the noise.

Templates aren't a permanent workaround, either

Once you're relying on approved templates to message outside the 24-hour window, there's another failure surface: templates go through Meta's approval process, and that approval isn't a one-time, permanent state. Templates can be flagged, paused, or fail re-review - from quality-rating issues, policy changes, or content that no longer matches what's on file. A workflow hardcoded to a specific template name will start failing the moment that template's status changes, and again, that failure often surfaces as a delivery problem rather than an obvious "your template broke" error unless you're explicitly checking template status as its own signal.

Why "workflow succeeded" can still mean the customer got nothing

Put these together and you get a specific, recurring failure pattern: a workflow engine calls the WhatsApp API, gets a 200, marks the step as successful, and moves the customer record forward - lead marked "contacted," ticket marked "responded to," sequence marked "complete." Every system-level signal says the job is done.

But "the API call succeeded" and "the customer was reached" are two different claims, and only webhook-confirmed delivery status can back up the second one. A workflow that treats API acceptance as the finish line will confidently report success on messages that were rejected for a closed window, failed on an unapproved template, or simply never delivered - and nothing in the workflow's own logs will show that it happened.

What this means for observability

If you're running WhatsApp automation in production, a few things are worth treating as first-class signals, not afterthoughts:

  • Track message state as a pipeline, not a boolean. Accepted -> sent -> delivered/failed -> read. Each transition is a fact you learn later, not at send time.
  • Split your alerting into two paths, not one. A synchronous rejection (the send call itself returns an error) is already visible at request time and should alert immediately. An asynchronous failed status only shows up later, on the webhook, after the API accepted the message - that path needs its own alert too, because nothing in your original request/response cycle will flag it.
  • Track window state explicitly, per conversation, so a send attempt outside the 24-hour window is a known, expected case in your code - not a mystery failure to debug later.
  • Monitor template status independently of message sends. A template moving from approved to paused should raise an alert before it silently blocks a whole flow.
  • Build a human handoff and recovery path for messages that don't resolve to delivered. Automatically retrying a failed send isn't always correct (a rejected free-form message needs a template, not a retry); the safer default is surfacing it to a person who can decide the right next action - and recovering gracefully matters as much as detecting the problem in the first place.

What to actually measure

In practice, the metrics worth watching in production are less about volume and more about the gap between stages: the percentage of accepted messages that never resolve to delivered, delivery latency distribution (not just averages - tail latency is where problems hide), failed-message reasons broken down by cause (window closed vs. template issue vs. genuine delivery failure), and how long messages sit in "pending" before resolving one way or the other. None of this is visible if you're only watching your own API's response codes.


We encountered these edge cases while building ReplyQ, where WhatsApp conversations have to remain observable beyond the automation layer itself. Getting the "accepted" response right was never the hard part - building the visibility to know what happened after that was: https://reply-q.ai

Top comments (0)