Description
What is the statuses webhook?
The statuses webhook is a notification on the messages field whose value contains a statuses array instead of a messages array — the same field as the messages webhook, with the opposite job. Each item reports one step in the life of a message your business sent: id (the wamid the send call returned), status, timestamp and recipient_id. The status is sent, delivered, read or failed, plus played the first time a recipient plays a voice message. A message can trigger up to three success statuses — sent, delivered and read (plus played for a voice message) — or a single failure, and when a message is read the moment it arrives Meta skips the delivered webhook, because the read implies it.
Two objects make statuses more than ticks. The webhook pricing object, pricing, rides on the sent status and on one of the later delivered or read statuses, and says how the message is billed: pricing_model (PMP, per-message pricing, for everything since 1 July 2025), type (regular or free_entry_point, plus free_customer_service for in-window replies before 1 October 2026) and category (marketing, utility, authentication, authentication-international, service, marketing_lite or referral_conversion). Meta says its billable flag will be deprecated in a future version. The errors array, one of the webhook error objects, appears on failed statuses with code, title, message, error_data.details and an href to Meta's error-code page.
Several fields are conditional. recipient_user_id carries the user's business-scoped user ID, biz_opaque_callback_data echoes a tracking string you set when sending, and group messages add recipient_type: group and recipient_participant_id. On webhooks for Graph API v24.0 and later the old conversation object is omitted except inside a free entry point window. This page is about the payload; the message status page explains what the ticks mean to a person reading the chat.
Key takeaways
- A statuses webhook reports what happened to a message you sent — sent, delivered, read or failed — keyed by its wamid.
- Statuses arrive on the
messagesfield in astatusesarray, often batched and never guaranteed to be in order. - The
pricingobject rides onsentand on one ofdeliveredorread, giving the billingcategoryandtype. - Failed statuses carry an
errorsarray with a numericcode, such as 131049 for per-user marketing limits. - Rank sent < delivered < read, only ever move forward, and deduplicate on wamid plus status.
Usage
The statuses webhook in code
const RANK = { sent: 10, failed: 15, delivered: 20, read: 30, played: 40 }; // failed only beats sentexport async function onStatuses(change) {for (const s of change.value.statuses ?? []) {const key = s.id + ":" + s.status;if (await seen(key)) continue; // retries resend statusesconst msg = await outbound.byWamid(s.id); // saved when you sent itif (!msg) { await orphans.save(s); continue; } // a status can beat your insertif (RANK[s.status] > (RANK[msg.status] ?? 0)) { // never move backwardsawait msg.update({ status: s.status, at: Number(s.timestamp) * 1000 });}if (s.pricing && !msg.pricing) { // first seen on sent; repeated on delivered or readawait msg.update({ pricing: { category: s.pricing.category, type: s.pricing.type } });}if (s.status === "failed") {const e = s.errors?.[0];await failures.record(msg, e?.code, e?.error_data?.details);}}}
Deduplicate on wamid plus status, only move forward, record pricing once and file failures by code.
Mechanics
How does the statuses webhook work?
step 1 — Store the wamid at send time
The send call returns
messages[0].id. Save it with your own reference (order ID, campaign ID) before any status can arrive, so every webhook can be joined back to a business record.step 2 — Recognise statuses on the messages field
Statuses arrive on the same
messagesfield as inbound messages. Ifvalue.statusesexists, loop over every item; one POST can batch statuses for several messages.step 3 — Move status forward only
Rank
sent<delivered<readand only ever upgrade. Webhook event ordering is not guaranteed, so areadthat arrives before itsdeliveredmust not be overwritten by it.step 4 — Record pricing once
Take
pricing.categoryandpricing.typefrom the first status that carries them (normallysent), but count the message as billed only oncedeliveredorreadarrives. Reconcile on those fields, not onbillable, which Meta plans to deprecate.step 5 — Handle failures by code
On
failed, readerrors[0].codeanderror_data.details. Some codes need a different template or a pause (131049, for example, means wait at least 24 hours before retrying marketing to that user).
Properties
The statuses webhook: key facts and limits
- Arrives on
- messages field, value.statuses[]
- Status values
- sent, delivered, read, played, failed
- Join key
- statuses[].id = the wamid returned by the send call
- Pricing model
- PMP (per-message) since 1 July 2025
- Pricing object on
- sent, plus one of delivered or read
- Pricing type
- regular, free_entry_point (free_customer_service until 30 Sep 2026)
- conversation object
- Omitted on v24.0+ webhooks, except in a free entry point window
- Ordering
- Not guaranteed: a read can arrive before its delivered
Caveats
Common mistakes with the statuses webhook
Letting late statuses overwrite newer ones
Webhooks are not ordered. If
deliveredarrives afterreadand you simply save the latest webhook, the message moves backwards. Rank statuses and only upgrade.Expecting a read for every delivered message
Users can switch the read receipt setting off, and many simply never open a message. A missing
readis not a failure — report read rate against delivered, not against sent.Billing from billable or from sent
billableis slated for deprecation, and asentstatus carries pricing too, yet Meta bills delivered messages only. Count deliveries bypricing.categoryandpricing.type; since 1 October 2026 aservicecategory in India is charged at ₹0.145.Dropping statuses that beat your database write
A fast
sentwebhook can arrive before your own insert commits. Park unmatched statuses briefly and replay them, instead of discarding them as unknown wamids.
Why it matters
Why does the statuses webhook matter?
Statuses are the only record of whether a message actually reached someone. Support teams use them to answer "did the customer get the OTP?", marketers to measure delivery and read rates, and finance to reconcile Meta's invoice, since billing counts delivered messages only. A pipeline that drops, misorders or double-counts statuses produces wrong dashboards, wrong invoices and agents who cannot tell a failed send from a slow reader.
Example
Example: Proving an OTP arrived in Pune
A Pune lending app's customer says she never received her login code. The support agent searches by her number and sees the authentication template's wamid with three statuses: sent at 21:14:02, delivered at 21:14:04 with pricing.category: authentication, and no read yet, because she has either not opened the chat or switched read receipts off. The agent can say with confidence that the code reached her phone two seconds after it was sent, and suggests she check the chat rather than resending and paying for a second delivery.
Notes
Best practices for the statuses webhook
- Set biz_opaque_callback_data when sending campaigns; it comes back on the status and saves a lookup.
- Alert on a rising share of failed statuses per template, not just on individual failures.
- Store recipient_user_id beside recipient_id, since usernames can hide phone numbers.
- Keep failed error codes in reporting — 131049 and 131026 point to very different fixes.
Questions
Statuses Webhook FAQs
- Why do I get statuses on the messages field?
- Meta groups inbound messages and outbound statuses under one subscription field. The value object tells them apart: a messages array is inbound, a statuses array reports on what you sent.
- Which status means I am charged?
- The delivered (or read) status is the one that marks a charge, because Meta bills delivered messages only. The pricing object already appears on the sent status, so use it to classify the message, but do not count it as billed until delivery is confirmed.
- Why don't I see the conversation object any more?
- On webhooks for Graph API v24.0 and later Meta omits it, except for messages inside a free entry point window. With per-message pricing, billing no longer depends on a conversation ID.
- Can a message go from failed to delivered?
- Treat failed as final for that wamid. If you resend, the new message has a new wamid and its own statuses.
* Per-message rates for India, ex-GST: marketing, utility and authentication effective 1 July 2026; service messages ₹0.145 since 1 October 2026 (they were ₹0 until 30 September 2026). Volume commitments earn discounts — final rate is confirmed on your account; applicable GST extra. Rates for other countries differ (see the international rate table on /pricing).