Why does n8n show success when the WhatsApp message never arrived?
Short answer: the WhatsApp Cloud API answers your send request as soon as it accepts the message, so the n8n node goes green. Whether the message was actually delivered is reported later, in a separate status webhook (sent, delivered, read or failed). If your workflow never listens to that webhook, a failed message looks exactly like a successful one. Fix it by saving the message ID from every send, recording each status update against that ID, and alerting on failed or on messages that stay stuck without delivered.
This came up repeatedly in r/n8n on 12 November 2026. One builder wrote that the WhatsApp node returns 200 and the execution is green, but the message fails a few seconds later and "the real error only shows up in the status webhook, which most flows ignore" (thread). The same day, another self-hoster described a lead workflow that quietly dropped leads for 30 hours and cost them their first client (thread).
"Accepted" is not "delivered"
| What you see | What it really means |
|---|---|
| Green execution in n8n | Every node ran without throwing |
| HTTP 200 and a message ID from the send call | Meta accepted the request into its queue |
sent status webhook |
Meta sent it toward the recipient |
delivered status webhook |
It reached the recipient's device |
failed status webhook |
It did not go out; the errors field says why |
Common reasons a send is accepted and then fails: the 24-hour customer service window has closed and you sent free text instead of an approved template, the template was paused or rejected, the number is not on WhatsApp, or the account hit a messaging limit. None of these make the n8n node red.
The same gap exists for email (accepted by the provider, then bounced) and SMS (queued, then undelivered). The pattern below works for all three.
The pattern: log the ID, listen for status, reconcile
-
Log every send. Right after the WhatsApp node, write one row: message ID, recipient, workflow name, execution ID,
sent_at, and statusaccepted. -
Subscribe to status webhooks. In your Meta app, point the WhatsApp webhook at an n8n Webhook trigger (verify the request signature before trusting it). Each payload under
statusescarries the message ID and the new status. -
Update by message ID, never insert blindly. Webhooks can arrive late, twice, or out of order. Only move a row forward (
acceptedtosenttodeliveredtoread), and letfailedoverride anything, storing the error code and title. -
Alert on two conditions. A row turns
failed: alert immediately with the recipient and error. A row is stillacceptedorsentafter a set time (for example 15 minutes for chat replies): alert once, because either delivery or your webhook is broken. -
Close the loop for important messages. For leads or invoices, a
failedrow should create a follow-up task or fall back to email, not just ping Slack.
The table
CREATE TABLE IF NOT EXISTS msg_delivery (
message_id TEXT PRIMARY KEY,
channel TEXT NOT NULL DEFAULT 'whatsapp',
recipient TEXT NOT NULL,
workflow TEXT NOT NULL,
execution_id TEXT,
status TEXT NOT NULL DEFAULT 'accepted',
error_code TEXT,
error_title TEXT,
sent_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
alerted_at TIMESTAMPTZ
);
Forward-only status update from the webhook workflow:
UPDATE msg_delivery
SET status = $2, error_code = $3, error_title = $4, updated_at = now()
WHERE message_id = $1
AND (
$2 = 'failed'
OR array_position(ARRAY['accepted','sent','delivered','read'], $2)
> array_position(ARRAY['accepted','sent','delivered','read'], status)
);
Stuck-message check, run on a schedule, which claims each alert once:
UPDATE msg_delivery
SET alerted_at = now()
WHERE alerted_at IS NULL
AND (status = 'failed'
OR (status IN ('accepted','sent') AND sent_at < now() - interval '15 minutes'))
RETURNING message_id, recipient, workflow, status, error_code, error_title;
Quick test
Send a free-text message to a test number that has not messaged you in the last 24 hours. The n8n execution should still be green, and within a short time your table should show failed with an error, and you should get exactly one alert. If you get nothing, your webhook is not wired up, and that is the problem this whole article is about.
How this fits with a heartbeat watchdog
A heartbeat watchdog tells you the workflow ran and processed items. Delivery tracking tells you the last step actually reached a person. You want both: one catches "nothing happened", the other catches "something happened but nobody got it".
Doing it in Make
The same pattern works in Make: a scenario sends the message and adds a row to a Data Store or Google Sheet, a second scenario with a custom webhook receives the WhatsApp status updates and updates the row, and a scheduled third one alerts on failed or stuck rows. You can build it on the free plan: Make free plan.
Disclosure: Some links are affiliate links — I may earn a commission at no extra cost to you.
Top comments (0)