Get in Touch

Have a question about the platform, need help with your integration, or want to discuss partnership opportunities and enterprise pricing? Drop us an email — we’ll do our best to get back to you within 3–4 hours.

contact@fiwano.com
Documentation menu

Working with an AI agent? Download the full documentation as a Markdown file to use as context.

Download full .md

One Delivery Status for Three Chat APIs: Unifying WhatsApp, Instagram & Messenger Webhooks

"Was my message delivered? Was it read?" sounds like one question with one answer. Build on top of WhatsApp, Instagram and Facebook Messenger directly and you discover it is three questions with three different answers — and the differences are not just in the JSON. The behavior diverges: the channels disagree on which lifecycle states exist at all, on what "delivered" even means, on how a "read" receipt is addressed, and on whether a failure ever reaches you asynchronously.

This is a tour of where the three Meta messaging APIs actually diverge on status reporting, and the work it takes to collapse them into a single webhook contract. If you've integrated one of these channels, the other two will surprise you in exactly the ways below.

The four words that don't mean the same thing

A delivery lifecycle reads the same on paper everywhere — sent → delivered → read, plus failed. Here is what each channel actually reports on Meta's own API — this table is the raw platform behavior, before any unifying layer (ours or anyone's) touches it:

Meta API reports WhatsApp Messenger Instagram
sent (accepted by Meta) statuses[].status=sent ❌ never ❌ never
delivered (on device) statuses[].status=delivered message_deliveries ❌ no delivery event
read ✅ per-message, privacy-gated message_reads, watermark messaging_seen, per-message
failed (asynchronous) statuses[].status=failed ❌ none ❌ none
addresses a message by id (Meta mid) mids[] / watermark timestamp echo mid / read mid

Four columns of asymmetry, straight from Meta. Each one is a behavioral landmine, not a formatting nuisance — and the rest of this page is about what it takes to hide them behind one contract.

Divergence 1: which states even exist

WhatsApp gives you the full lifecycle: an explicit sent the moment Meta accepts the message, a delivered when it reaches the device, a read, and an asynchronous failed.

Messenger and Instagram give you two states, not four. There is no sent — the closest thing is the synchronous HTTP response from the Send API. And, crucially, there is no asynchronous failed event (more on that below). So a naive integration that waits for a terminal read or failed on Instagram will wait forever: those signals are structurally absent. The first status you can ever observe on IG/Messenger is delivered.

Divergence 2: "delivered" is not one thing

On WhatsApp and Messenger, delivered is a genuine device-level receipt: Meta is telling you the message reached the recipient's phone.

Instagram has no delivery receipt at all. Its messaging webhook exposes messaging_seen (read) and message echoes — but nothing that says "delivered." So where does a delivered event come from? We synthesize it. When you send an Instagram DM, Meta echoes your own message back to your webhook (message_echoes, is_echo: true) roughly 1–3 seconds later, once the message has been accepted into the conversation thread. That echo is the earliest and best "it's in the system" signal Instagram offers, so we treat it as delivered.

Be honest about what that means: Instagram's delivered is "accepted into the thread," not "on the recipient's device." It is the strongest signal the platform gives, but it is not semantically identical to the WhatsApp/Messenger receipt. A unified API should expose one event — and document the asterisk rather than pretend the channels are the same.

There's a second hazard hiding in the echo trick. A single Instagram account can have several apps attached (your integration, a Meta Business Suite session, the IG mobile app, another vendor). Meta echoes every outbound message on that account to every subscribed app. So you receive echoes for messages you never sent. Treating those as your own delivered events would emit phantom statuses for messages that don't exist in your system. The fix is to resolve every echo's mid against your own outbound records and drop the foreign echoes — only echoes that match a message you actually sent become a delivered event.

Divergence 3: "read" is addressed three different ways

WhatsApp and Instagram report reads per message: the receipt carries the message id, and you mark that one message read.

Messenger doesn't send message ids on reads. It sends a watermark — a timestamp that means "everything in this conversation up to this moment has been read." One read event can therefore stand for a dozen messages. To map it onto per-message status, you have to expand the watermark: look up every message you sent to that user at or before the watermark timestamp that isn't already read or failed, and emit a read for each. One inbound Meta event fans out into many outbound status events — what we call read cascading.

And reads are not guaranteed on every channel. WhatsApp read receipts depend on the recipient's privacy setting: if they've turned read receipts off, the read status never arrives, no matter that they read the message. Any logic that blocks on read as a terminal state will hang on those users — delivered is the only safe terminal success signal to design around.

Divergence 4: failure is WhatsApp's problem alone

This is the difference that bites hardest in production. WhatsApp reports failures asynchronously: a message can be accepted at send time, then fail later in delivery and arrive as a failed status with a Meta error code. In multi-device setups WhatsApp can even report delivered and failed for the same message — delivered to one device, failed on another. Your handler has to tolerate non-linear lifecycles.

Messenger and Instagram have no asynchronous failure channel. If a send fails, you learn about it exactly once: in the synchronous HTTP response to your Send API call. After Meta accepts the message, silence means success — there is no later event that retracts it. So the same logical question, "did this message fail?", is answered by a webhook on one channel and only by an API return code on the other two. A unified layer has to bridge that: surface WhatsApp's async failed as an event, and treat the synchronous send error as the failure signal everywhere else.

Divergence 5: every status names the message differently

WhatsApp delivery statuses reference Meta's mid. Messenger deliveries reference mids[]. Messenger reads reference no id at all — just the watermark timestamp. Instagram references the echo mid and the read mid. None of these is the id you got back when you sent the message in a unified API — and they're inconsistent with each other.

So the unifying move is to mint our own UUID at send time, return it from the send call, and reconcile every incoming Meta status back to it: a Meta mid is looked up against an in-memory cache first (zero database hits on the hot path) and the database as fallback; a Messenger watermark is resolved by (channel, recipient, time-range). From the caller's side, the message you sent and every status that follows it carry the same id, regardless of which of the three identity schemes Meta used underneath.

What the unified contract looks like

After all of the above, the client sees one shape. Every status — on any channel — arrives at your webhook_url as the same envelope:

{
  "event": "message.delivered",
  "channel_id": 42,
  "channel_type": "instagram",
  "timestamp": "2026-06-25T10:00:05Z",
  "data": {
    "message_id": "a1b2c3d4-...",
    "status": "delivered",
    "recipient": "..."
  }
}

message_id is always the UUID from your send response. event is one of message.sent, message.delivered, message.read, message.failed. The full per-channel event matrix and payload fields live in Receiving Messages — this article is the why behind that contract.

A few non-obvious properties fall out of unifying real channels rather than an idealized one:

  • Idempotency. Meta retransmits status webhooks if you don't acknowledge fast enough (during a deploy, say). Each (message, status) transition is de-duplicated so a retransmit never doubles an event.
  • Honest caveats over false uniformity. message.sent and message.failed fire on WhatsApp only. message.delivered and message.read fire on all three — but Instagram's delivered is the echo signal, and read may never come on WhatsApp if receipts are off. We expose one API and document the edges, instead of faking states the platform doesn't emit.
  • Different freshness SLAs underneath. Meta drops a Messenger webhook subscription after 1 hour of failed delivery, Instagram after 36 hours, while WhatsApp retries for up to 7 days. The same downtime degrades the three channels differently — the unified layer re-subscribes defensively so statuses keep flowing after a deploy or outage.

The takeaway

"Unified messaging API" usually sells the easy half — one endpoint to send. The hard half is the part you only hit after the message leaves: three platforms that disagree on which delivery states exist, what "delivered" means, how a read is addressed, and whether failure is ever reported asynchronously. Collapsing that into one webhook with one stable id, while being honest about the seams, is most of the actual engineering.

Ready to build on it? Sending Messages covers the send side, Receiving Messages is the full status and webhook contract, and you can start for free.

Frequently asked questions

What delivery statuses do WhatsApp, Instagram and Messenger report?

They differ. WhatsApp reports the full lifecycle — sent, delivered, read and an asynchronous failed. Messenger and Instagram report only delivered and read: there is no 'sent' event and no asynchronous failure event on those two channels.

Does the Instagram API have a delivery receipt?

No. Instagram's messaging webhook exposes a read receipt (messaging_seen) and message echoes, but no delivery event. A 'delivered' signal has to be synthesized from the echo Meta sends back about 1–3 seconds after the message is accepted into the conversation thread — so it means 'accepted into the thread', not 'on the device'.

Why is my WhatsApp message not marked as read?

WhatsApp read receipts depend on the recipient's privacy setting. If they have read receipts turned off, the read status never arrives even though they read the message. Treat delivered as the terminal success state; don't block on read.

How do Messenger read receipts work?

Messenger reports reads with a watermark — a timestamp meaning 'everything up to here has been read' — instead of per-message IDs. A single read event therefore covers every message sent at or before that timestamp, so it has to be expanded into one read status per message.

How does Fiwano unify delivery statuses across the three channels?

Fiwano maps every channel onto one webhook contract: message.sent, message.delivered, message.read and message.failed, each referencing the same UUID you got from the send call. It synthesizes Instagram's delivered from echoes (dropping echoes from other apps on the account), expands Messenger's read watermark per message, and surfaces WhatsApp's asynchronous failures — while documenting which events fire on which channel.

Fiwano API Documentation