Disclosure: I run NxFlowAI. Field names below are simplified; check your provider's payloads before copying anything.
Most WhatsApp to CRM writeups stop at inbound messages. The other half is status events for the messages you send: sent, delivered, read, failed. Meta's Cloud API reports these through the same messages webhook field as inbound messages, and they create their own mess in a CRM if you write them naively. This is the part of WhatsApp Business API CRM integration with idempotent writes that teams usually discover in production.
Three properties of status events
- They repeat. Meta's webhook docs say undelivered webhooks are retried for up to seven days and that retries can produce duplicates.
-
They arrive out of order. You may receive
readbeforedelivered, or a latedeliveredafterread. - They are noisy. One outbound template can produce several events. Writing each as a CRM activity buries the useful history.
Store status on the message, not as activities
CREATE TABLE outbound_messages (
provider_message_id TEXT PRIMARY KEY,
crm_contact_id TEXT NOT NULL,
template_name TEXT,
status TEXT NOT NULL DEFAULT 'accepted',
status_rank INT NOT NULL DEFAULT 0,
error_code TEXT,
updated_at TIMESTAMPTZ NOT NULL
);
Only move forward
RANK = {"accepted": 0, "sent": 1, "delivered": 2, "read": 3}
def on_status(evt):
msg = db.get(evt.message_id)
if msg is None:
return park(evt) # status before our own record: retry later
if evt.status == "failed":
db.update(msg.id, status="failed", error_code=evt.error_code)
return alert_owner(msg, evt) # failures are the only status a person must see
if RANK.get(evt.status, -1) > msg.status_rank:
db.update(msg.id, status=evt.status, status_rank=RANK[evt.status])
Duplicates and late events become no-ops. A failed always wins and always reaches a person.
What the CRM should show
- One activity per outbound message, created once when you send it.
- A status field on that activity, updated in place.
- A task for the conversation owner when a message fails, with the error code in plain words.
Do not write a separate CRM activity for every delivered or read. Sales teams stop reading timelines that look like logs.
Failed templates deserve their own view
Template sends can fail for reasons that have nothing to do with your code: a paused or rejected template, a recipient who cannot receive it, a policy limit. Keep a simple daily list of failed sends grouped by template and error code. A sudden cluster is often the first sign a template needs attention.
Test cases
-
readarrives beforedelivered: final statusread. - Same
deliveredevent three times: one update. - Status arrives before the send call returned: parked, then applied.
-
failedaftersent: statusfailed, owner alerted once.
In a 72-hour audit this is one of the first places we look, because failed sends are often invisible to the people who own the customer.
Top comments (0)