WhatsApp API, October 1: What Changes in Your Code When Meta Starts Billing Service Messages
Table of Contents
On October 1, 2026 the WhatsApp status webhook does not change shape. It changes meaning. The same pricing object that arrives today with "type": "free_customer_service" starts arriving with "type": "regular" in two cases that stay free through September 30: service messages from the 1,001st of the month on each business phone number, and utility templates you send inside the 24-hour customer service window. If your code hardcodes the service rate at zero, decides with billable, or drops statuses whose category it does not recognize, your cost report will say you spent nothing while Meta bills you.
There is plenty of coverage of the new prices. This post is about the other half: which fields to read, which values each one takes according to Meta’s documentation, how to count the free tier without inventing the rule, and what to check before September 30. The figures come from Meta’s Mexico rate card, in both of the currencies it publishes that row in: MXN and USD.
What is billed before and after October 1
Per delivered message to a +52 (Mexico) phone number. The numbers come from the official rate cards “effective July 1, 2026” and “effective October 1, 2026”, which I downloaded on September 13 from Meta’s pricing page (Mexico row). Meta publishes a separate card per billing currency; I quote the MXN and USD cards as published and do not convert between them. Use the card that matches your WABA’s currency, and the row that matches your recipients’ country.
| Message | Through Sep 30 (MXN / USD) | From Oct 1 (MXN / USD) | pricing.type from Oct 1 |
|---|---|---|---|
| Marketing template | 0.5614 / 0.0305 | 0.7298 / 0.0397 | regular |
| Utility template outside the window | 0.1565 / 0.0085 | 0.1565 / 0.0085 | regular |
| Utility template inside the 24h window | free | 0.1565 / 0.0085, from the first one | regular |
| Authentication template | 0.1565 / 0.0085 | 0.1565 / 0.0085 | regular |
| Service (non-template), 1st to 1,000th of the month per number | free | free | free_customer_service |
| Service (non-template), 1,001st onward | free | 0.1565 / 0.0085 | regular |
| Anything inside the 72h free entry point window | free | free | free_entry_point |
Three details that change how you write the code:
- The switch happens at 12am in your WABA’s timezone, not UTC. The pricing page says so for the whole October package: “Rate updates below apply as of 12am by WhatsApp Business Account (WABA) timezone”.
- The 1,000 free tier covers service messages only. Meta describes it as “a free monthly tier of 1,000 service messages per business phone number”, with no roll-over. It announces no free tier for utility: a utility template inside the window is billed from the first one.
- Mexico’s marketing rate rises about 30% (0.5614 → 0.7298 MXN; 0.0305 → 0.0397 USD). It is the only rate change in that row. Volume tiers will not help a small business either: in the October tiers card, Mexico utility is charged at list rate from message 0 to 1,000,000 a month, and service messages have no tiers at all per the non-template messages page.
The real payload, and the fields that matter
This is a service message delivered after the monthly free tier is used up. The envelope follows the status messages webhook reference, and the pricing object is verbatim the one Meta publishes for “Paid service message”. IDs and phone numbers are the reference’s own example values; the timestamp is October 14, 2026.
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "102290129340398",
"changes": [
{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "15550783881",
"phone_number_id": "106540352242922"
},
"statuses": [
{
"id": "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQUFERjg0NDEzNDdFODU3MUMxMAA=",
"status": "delivered",
"timestamp": "1792002600",
"recipient_id": "16505551234",
"pricing": {
"billable": true,
"pricing_model": "PMP",
"type": "regular",
"category": "service"
}
}
]
},
"field": "messages"
}
]
}
]
}
The same message in September, or inside the free tier, carries "billable": false and "type": "free_customer_service". A utility template inside the window flips the same way: free_customer_service through September 30, regular from October 1, with "category": "utility".
pricing.type: the one that decides
This field tells you why a message was or was not billed. The status reference documents three values:
regular: billed.free_customer_service: free because it went out inside the customer service window. From October, that includes being inside the 1,000 free tier.free_entry_point: free because of the 72-hour window opened by Click-to-WhatsApp ads and Facebook Page call-to-action buttons.
The pricing page adds a fourth that the reference (last updated May 21) does not list yet: free_group_customer_service, for group messages.
pricing.category: which rate was applied
Reference values: authentication, authentication-international, marketing, marketing_lite, referral_conversion, service and utility. Watch authentication-international: in pricing.category it uses a hyphen, while the conversation category in conversation.origin.type uses an underscore. The pricing page adds group_service and group_utility.
The practical consequence: do not validate these fields against a closed list. If your parser drops a status because the category is not in your enum, that message ends up unpriced in your database while Meta still bills it.
pricing.billable: do not decide with it
The reference is explicit: “The billable property will be deprecated in a future versioned release. Use pricing.type and pricing.category together to determine whether a message is billable and, if so, its billing rate.” Today billable and type agree. The day Meta removes it in a new Graph API version, an if (billable === false) return 0 turns into “everything is billed” or “nothing is billed”, depending on how you treat undefined.
When the object arrives, and when it counts
- Per the reference syntax,
pricingis “only included with sent status, and one of either delivered or read status”. The v24.0 example adds that it can show up only ondeliveredand not onsent. So you get it twice or once, and you must deduplicate bywamid. - Meta bills what is delivered, not what is sent (“only when the message is delivered (vs. sent)”). And
deliveredmay never arrive: if the user has the chat open, Meta sendsreaddirectly. Count a message as delivered when eitherdeliveredorreadshows up. - Since v24.0 the
conversationobject is omitted unless the message is inside a free entry point window. If your code took the category fromconversation.origin.type, it was already behind.
The four bugs that make your report say zero
While reviewing the WhatsApp agent I have in development for this post, I found two of these in my own code. All four are easy to ship:
- A rate table with no effective date. A
service: 0constant is correct through September 30 and wrong from October 1, even though the webhook already saysregular. Meta can only change pricing on the first day of a quarter (January, April, July, October), with one month’s notice for a rate card update: model the card with its effective date. - Deciding with
billable. Works today; breaks when Meta retires it. - A category allowlist.
group_service,free_group_customer_serviceor the hyphen inauthentication-internationalend up as “no category” and add zero to your total. - Counting every status. If you add to the total on every webhook that carries
pricing, one message counts twice (sent+delivered), and a message that failed after being sent counts even though Meta does not bill it.
Code: counting billable messages per number and per month
The approach mirrors the cost ledger in my agent: one row per outbound message, filled in by whatever each status carries, with cost computed at read time. It targets Cloudflare D1 (SQLite), but the SQL is portable.
The table first. category and pricing_type are stored exactly as received, with no allowlist:
CREATE TABLE wa_outbound (
wamid TEXT PRIMARY KEY,
phone_number_id TEXT NOT NULL,
month TEXT NOT NULL, -- 'YYYY-MM' in the WABA timezone
delivered INTEGER NOT NULL DEFAULT 0, -- 1 once `delivered` or `read` arrives
category TEXT, -- pricing.category, not normalized
pricing_type TEXT -- pricing.type, not normalized
);
CREATE INDEX wa_outbound_month ON wa_outbound (phone_number_id, month);
Then what you do with each statuses[] entry. phone_number_id comes from value.metadata.phone_number_id, which is the unit the free tier is counted on:
type Open<T extends string> = T | (string & {}); // autocompletes, but accepts whatever Meta adds
export interface StatusPricing {
/** Meta will deprecate it: decide with `type` + `category`. */
billable?: boolean;
pricing_model: Open<'PMP' | 'CBP'>;
type: Open<'regular' | 'free_customer_service' | 'free_entry_point' | 'free_group_customer_service'>;
category: Open<'service' | 'utility' | 'marketing' | 'marketing_lite' | 'authentication' | 'authentication-international' | 'referral_conversion' | 'group_service' | 'group_utility'>;
}
export interface WebhookStatus {
id: string; // wamid
status: Open<'sent' | 'delivered' | 'read' | 'failed' | 'played'>;
timestamp: string; // epoch seconds, as a string
recipient_id: string;
pricing?: StatusPricing;
}
/** 'YYYY-MM' in your WABA timezone, e.g. 'America/Mexico_City'. */
export function monthKey(epochSeconds: number, timeZone: string): string {
return new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit' })
.format(new Date(epochSeconds * 1000))
.slice(0, 7);
}
const DELIVERED = new Set(['delivered', 'read']); // `read` without `delivered` still means delivered
export async function applyStatus(db: D1Database, phoneNumberId: string, s: WebhookStatus, timeZone: string) {
await db
.prepare(
`INSERT INTO wa_outbound (wamid, phone_number_id, month, delivered, category, pricing_type)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
ON CONFLICT(wamid) DO UPDATE SET
month = CASE WHEN excluded.delivered = 1 AND delivered = 0 THEN excluded.month ELSE month END,
delivered = MAX(delivered, excluded.delivered),
category = COALESCE(excluded.category, category),
pricing_type = COALESCE(excluded.pricing_type, pricing_type)`,
)
.bind(
s.id,
phoneNumberId,
monthKey(Number(s.timestamp), timeZone),
DELIVERED.has(s.status) ? 1 : 0,
s.pricing?.category ?? null,
s.pricing?.type ?? null,
)
.run();
}
The ON CONFLICT does the dirty work: repeated statuses do not duplicate, pricing is kept whether it came on sent or on delivered, and the month that counts is the delivery month (a message sent at 11:59pm on September 30 and delivered on October 1 lands in October).
Finally, the monthly bill. The rate card carries a date, and what decides whether a message is billed is type, not your counter:
const FREE_SERVICE_PER_MONTH = 1000;
/** Mexico card in MXN per delivered message. One entry per quarter with changes. */
const MX_RATES_MXN: Array<{ from: string; rates: Record<string, number> }> = [
{ from: '2026-07', rates: { marketing: 0.5614, utility: 0.1565, authentication: 0.1565 } },
{ from: '2026-10', rates: { marketing: 0.7298, utility: 0.1565, authentication: 0.1565, service: 0.1565 } },
];
const ratesFor = (month: string) => MX_RATES_MXN.findLast((card) => card.from <= month)?.rates ?? {};
export async function monthlyBill(db: D1Database, phoneNumberId: string, month: string) {
const { results } = await db
.prepare(
`SELECT category, pricing_type, COUNT(*) AS n FROM wa_outbound
WHERE phone_number_id = ? AND month = ? AND delivered = 1
GROUP BY category, pricing_type`,
)
.bind(phoneNumberId, month)
.all<{ category: string | null; pricing_type: string | null; n: number }>();
const rates = ratesFor(month);
let total = 0;
let serviceDelivered = 0;
let paidService = 0;
const unpriced: Record<string, number> = {};
for (const { category, pricing_type, n } of results) {
if (!category || !pricing_type) {
unpriced['(no pricing)'] = (unpriced['(no pricing)'] ?? 0) + n;
continue;
}
// Meta does not document whether entry point messages use up the tier: not counted here.
if (category === 'service' && pricing_type !== 'free_entry_point') serviceDelivered += n;
if (pricing_type !== 'regular') continue; // free: window, tier or entry point
if (category === 'service') paidService += n;
const rate = rates[category];
if (rate === undefined) {
unpriced[category] = (unpriced[category] ?? 0) + n; // billed and you have no rate: do not add it as 0
continue;
}
total += n * rate;
}
return {
total: Math.round(total * 100) / 100,
serviceDelivered,
freeServiceLeft: Math.max(0, FREE_SERVICE_PER_MONTH - serviceDelivered),
paidService,
unpriced,
};
}
If your WABA bills in USD, swap the card for the USD row (0.0305 / 0.0085 / 0.0085 from July, 0.0397 / 0.0085 / 0.0085 / 0.0085 from October). With that in place, alerting is one line in a daily cron: fire when freeServiceLeft drops below 200, when the month’s first paidService appears, or when unpriced is not empty. And if paidService is above zero while serviceDelivered is under 1,000, your counter and Meta disagree: another app sending from the same number, lost webhooks, or a month boundary in a different timezone. Meta wins.
To reconcile at month end, the WABA’s pricing_analytics field accepts the PHONE, PRICING_CATEGORY and PRICING_TYPE dimensions. Two caveats from the same page: the data is approximate (“may differ from what’s shown on invoices”), and COST is not returned if your WABA shares a Solution Partner’s credit line. Also, that reference (last updated June 11) still describes SERVICE as “Messages that were not charged”; the non-template messages page already shows the query with REGULAR + SERVICE.
What it costs: a small business with 600 conversations a month
A clinic or a repair shop with an agent answering WhatsApp, one phone number, customers on +52, and no Click-to-WhatsApp ads. Per month:
- 600 conversations with 5 service messages from the agent each: 3,000 service messages.
- 600 order or appointment confirmations as utility templates, sent inside the window.
- 400 reminders as utility templates, sent outside the window.
- One marketing campaign to 500 contacts.
I assume everything is delivered; in practice it comes out slightly lower, since Meta only bills delivered messages. List rates, before any taxes.
September (July rate card):
| Line | Math | MXN | USD |
|---|---|---|---|
| Service | 3,000 × 0 | 0.00 | 0.00 |
| Utility inside the window | 600 × 0 | 0.00 | 0.00 |
| Utility outside the window | 400 × 0.1565 MXN / 0.0085 USD | 62.60 | 3.40 |
| Marketing | 500 × 0.5614 MXN / 0.0305 USD | 280.70 | 15.25 |
| Total | 343.30 | 18.65 |
October (October rate card):
| Line | Math | MXN | USD |
|---|---|---|---|
| Service | (3,000 − 1,000) = 2,000 × 0.1565 MXN / 0.0085 USD | 313.00 | 17.00 |
| Utility inside the window | 600 × 0.1565 MXN / 0.0085 USD | 93.90 | 5.10 |
| Utility outside the window | 400 × 0.1565 MXN / 0.0085 USD | 62.60 | 3.40 |
| Marketing | 500 × 0.7298 MXN / 0.0397 USD | 364.90 | 19.85 |
| Total | 834.40 | 45.35 |
Meta’s bill goes from 343.30 to 834.40 MXN on the peso card (18.65 to 45.35 on the dollar card): 491.10 MXN or 26.70 USD more per month. Nobody goes under over that, but it is 2.4 times September, and 406.90 of those 491.10 pesos (over 80%) come from two lines that cost zero today.
Where the real lever is: your code
Switching the in-window utility confirmations to free-form text saves nothing in this example: in Mexico the service rate and the utility rate are the same, and the free tier is already used up. It only helps businesses that stay under 1,000 service messages a month.
What actually moves the number is how many messages your agent sends per reply. Plenty of AI agents split each answer into three or four bubbles because it reads more naturally. From October 1, every bubble past the 1,000th is a charge. If the same agent answers with 3 messages per conversation instead of 5:
- 600 × 3 = 1,800 service messages.
- (1,800 − 1,000) = 800 × 0.1565 = 125.20 MXN (800 × 0.0085 = 6.80 USD), versus 313.00 MXN (17.00 USD).
- Savings: 187.80 MXN or 10.20 USD a month, without touching a single price.
If some of your customers arrive through Click-to-WhatsApp ads, replying within the first 24 hours opens the 72-hour free entry point window, and everything in it comes back as free_entry_point. Meta confirms that window is unchanged for message delivery.
What happens with no payment method
Meta has two wordings here, from different dates, and they are worth reading together.
The non-template messages page, updated August 25, 2026, says:
For any Solution Provider or directly-integrated businesses that does not have a payment method on file by September 30, 2026, Meta will stop delivering service messages as of when they become charged on October 1, 2026. To avoid disruptions to your service messages, please add a payment method for your WhatsApp Business Account(s) by September 30, 2026.
The pricing page, updated September 10, 2026, which is where the free tier was introduced as new, is more specific:
If you do not have a payment method for your WhatsApp Business account: Meta will deliver your first 1,000 service messages each month but not deliver as of your 1,001st.
They do not contradict each other: the first says delivery stops “as of when they become charged”, and the second, written after the free tier was added, says that happens at the 1,001st message. In practice:
- The date Meta gives for having a payment method in place is September 30. Do not wait for the first failure.
- Without a payment method, your agent works normally until the number crosses 1,000 service messages for the month, and then stops delivering mid-month. For a busy business that can be a Tuesday afternoon.
- In the pages I reviewed, Meta does not publish which error code the
failedstatus carries in that case. Watch for a spike infailedon service messages from October 1. - The August notice is addressed to “Solution Provider or directly-integrated businesses”. If your WABA is billed through a solution provider, get written confirmation from them that the payment method is sorted before September 30.
Checklist before September 30
- A payment method in Billing Hub for every WABA, or written confirmation from your provider if you are billed through one.
- A tolerant parser: store
pricing.typeandpricing.categoryas received. Never drop a status because it carriesgroup_service,free_group_customer_serviceorauthentication-international. - Stop deciding with
billable. Usetype === 'regular'plus the rate forcategory. - A rate card with effective dates, loaded with the October card (Mexico marketing at 0.7298 MXN / 0.0397 USD). The next date Meta can change prices is January 1, 2027.
- Count on
deliveredorread, once perwamid, grouped byphone_number_idand by month in your WABA timezone. - Alerts: fewer than 200 free service messages left, first
regularservice message of the month, categories with no rate, and a rise infailed. - Utility templates inside the window: from October 1 they are billed from the first one. If you send fewer than 1,000 service messages a month, free-form text inside the window can be free where the template costs.
- Marketing versus utility templates: Meta bills the category a template has at the time of use, and can recategorize it. Subscribe to the
template_category_updatewebhook, which warns you 24 hours before an automated category change. - Messages per reply: merge your agent’s bubbles. It is the cheapest lever in the example.
- Monthly reconciliation against
pricing_analytics, knowing it is approximate.
Further reading:
- Build an AI agent on WhatsApp for your small business (Laravel + OpenAI): the webhook, conversation memory and the 24-hour window rules, from scratch.
Frequently asked questions
What changes in the WhatsApp Business API on October 1, 2026?
Meta starts billing two kinds of message that are free today: service messages (non-template messages sent inside the 24-hour customer service window) from the 1,001st message each month per business phone number, and utility templates sent inside that window, which are billed from the first one. In Mexico both cost 0.1565 MXN (0.0085 USD) per delivered message. Mexico marketing also rises from 0.5614 to 0.7298 MXN (0.0305 to 0.0397 USD). The change applies at 12am in your WhatsApp Business account timezone.
How do I know from the webhook whether a WhatsApp message was billed?
Read pricing.type in the status messages webhook: regular means billed; free_customer_service, free_entry_point or free_group_customer_service mean it was free. pricing.category tells you which rate was applied. Do not rely on pricing.billable: Meta’s reference says it will be deprecated in a future versioned release and recommends using type and category together.
Are the 1,000 free WhatsApp service messages per account or per phone number?
Per business phone number. Meta says every business phone number receives 1,000 free service messages per month, that it charges from the 1,001st service message delivered that month, and that unused messages do not roll over. The free tier only covers service messages; utility templates inside the window have no free tier.
What happens if my WhatsApp Business account has no payment method on October 1?
According to Meta’s pricing page, updated September 10, 2026, Meta delivers your first 1,000 service messages each month and stops delivering from the 1,001st. The non-template messages page asks businesses to add a payment method in Billing Hub by September 30, 2026. If you are billed through a solution provider, confirm with them that the payment method is in place.
Does the pricing object arrive on every status webhook?
No. Per Meta’s reference, pricing is included with the sent status and with one of delivered or read, and in v24.0 it can appear only on delivered. Since Meta bills delivered messages and sometimes sends read without delivered, count each message once per wamid when delivered or read arrives, and keep the pricing from whichever status carried it.
How much will a small business in Mexico pay Meta from October?
It depends on volume. With 600 conversations a month at 5 service messages each, 600 utility confirmations inside the window, 400 utility reminders outside it and one marketing campaign to 500 contacts, the Meta bill goes from 343.30 MXN in September to 834.40 MXN in October at list rates, or from 18.65 to 45.35 USD on the USD card. Cutting the agent from 5 to 3 messages per conversation lowers the service line from 313.00 to 125.20 MXN.
Should I replace utility templates with free-form text inside the window?
Only if you send fewer than 1,000 service messages a month. In Mexico, service and utility cost the same from October 1, but service gets 1,000 free messages per month per number and utility does not. Once the tier is used up, it makes no difference. The bigger lever is usually having your agent send fewer messages per reply.