The core idea
When you call the send API, you don't yet know what the message will cost. Meta assigns the price later, on the delivery status webhook.
One message, end to end
Vera Market sends an order-shipped template to a customer in Mexico.
| Time | What happens | What you know about the cost |
|---|---|---|
| 09:14:07 | The send call returns a message ID | Nothing |
| 09:14:08 | Meta delivers the message | Nothing |
| 09:14:09 | A status webhook arrives with a pricing object | The charge |
Two seconds, and for most messages that's the whole story. But the price only appears in the third row. Anything you logged at 09:14:07 has no cost on it.
Sometimes the gap is much longer, and occasionally the third row never arrives at all.
Reading the pricing object
"pricing": {
"billable": true,
"pricing_model": "PMP",
"type": "regular",
"category": "utility"
}
Three fields matter, and one of them is on the way out.
type
This is the authoritative field. regular means the message is billable. There are also explicitly free types:
free_customer_service: a service message inside the window. Free today; this is the type that October 2026 retires.free_entry_point: a conversation that began from a qualifying entry point, such as a click-to-WhatsApp ad.
category
Which of the categories from Chapter 1.1 applies. Combined with the recipient's country, this selects the rate.
billable
A legacy boolean that Meta is deprecating. Where type is present, type wins. Don't build new logic on billable. Read it only as a fallback for payloads that predate type.
Why this matters in practice
Your send log isn't your bill. Counting API calls tells you how many messages you sent, not what they cost. Any cost report has to be built from status webhooks. If you drop status webhooks, you lose billing data, not just delivery data.
If you bill through Kapso
Kapso builds that report from status webhooks already, and breaks the spend down by category, so you don't have to write it.
What it can't do is reach into your systems. If you attribute cost per customer, per campaign or per conversation in your own database, that part is still yours, and it still has to be keyed on the status webhook rather than the send.
The rate is the one in effect when the status arrives, not when you sent. For a message that sends at 23:58 and gets its status at 00:04, that distinction is normally invisible, but on a day when rates change it's the boundary that decides which rate applies.
Late statuses leave messages unpriced. A message with no status webhook has been delivered but has no price attached to it. Any system that tracks WhatsApp spend needs a policy for these, not just a happy path.
A missing pricing object means unknown, not free. Treating unknown as free is how you end up under-forecasting for a month and then getting a correction.
What to build on
Before the status webhook arrives, the cost of a message is unknown. Cost reporting built on status webhooks reconciles with Meta's invoice. Cost reporting built on send calls doesn't.