DEV Community

Ujjwal Dubey
Ujjwal Dubey

Posted on

Delivery Status Webhooks Into a CRM: Repeated, Out-of-Order and Failed Events

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

  1. They repeat. Meta's webhook docs say undelivered webhooks are retried for up to seven days and that retries can produce duplicates.
  2. They arrive out of order. You may receive read before delivered, or a late delivered after read.
  3. 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
);
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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

  • read arrives before delivered: final status read.
  • Same delivered event three times: one update.
  • Status arrives before the send call returned: parked, then applied.
  • failed after sent: status failed, 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)