Skip to content

API & Business Integrations

What our WhatsApp API integration got wrong

Easinvy Retail sent bills as WhatsApp Cloud API documents. Read against Meta's rules: the 24-hour window, templates, delivery webhooks, opt-in and price.

Ridam Agrawal

Founder, Easinvy

Published
Read
8 min
On this page

In short

What does a WhatsApp Business API integration need to send bills to customers?

A bill is a message the business starts, so the WhatsApp Cloud API delivers it as a free-form document only to a customer who messaged in the last 24 hours. Everyone else needs an approved utility template. The integration also needs a webhook for delivery status, a record of each customer's opt-in, and a PDF link that expires.

Easinvy Retail is the billing and inventory product we run for small shops in India. It sent bills to customers over the WhatsApp Business API, and the bills reached them. Today WhatsApp billing is set up for a retailer when they ask for it, rather than switched on for every shop, and before we build it again we read the version we shipped line by line against Meta's own documentation. Four things in it have to change. This note is that reading, with the code quoted from our repository.

What Easinvy Retail built

The integration had three parts, and the split between them is the part worth copying.

  • One central access token. Easinvy held a single Cloud API token in its environment, WHATSAPP_ACCESS_TOKEN. No shop ever handled one.
  • A Phone Number ID per shop. Each shop stored its own WhatsApp business number and the Phone Number ID Meta assigns to it, and only the shop's owner could change them. A send used the shop's number, so the customer saw the shop, not Easinvy.
  • The bill as a document message. The PDF went as a link inside a document message, with the bill's text as its caption.

This is the request the backend sent to https://graph.facebook.com/v22.0/<phone-number-id>/messages:

const payload = {
  messaging_product: "whatsapp",
  recipient_type: "individual",
  to,
  type: "document",
  document: {
    link: input.documentUrl,
    filename: input.filename,      // bill-<number>.pdf
    ...(input.caption ? { caption: input.caption } : {}),
  },
}

The body matches Meta's document message exactly, and the PDF sat at a public URL with a .pdf filename, which is what Meta needs to fetch it. The format was never the problem. What follows is about when WhatsApp lets that message through.

Indian numbers were normalised before sending: non-digits stripped, a leading 00 and a trunk 0 dropped, 91 added to a ten-digit number, and anything shorter than ten digits or longer than fifteen refused. The caption carried the shop's name, the bill number, the total and any amount still owed. It rounded both to whole rupees with Math.round, so a bill of ₹1,249.50 read ₹1,250, which is a smaller version of the problem in decimal vs float for money.

A bill opens no customer service window

WhatsApp sorts every message by who started the conversation. When a customer messages a business, a 24-hour customer service window opens, and inside it the business may send free-form messages, documents included. Meta's send-messages guide is plain about the rest: "When the window closes, you can only send pre-approved template messages."

A bill is the business speaking first. A customer who paid at the counter has not messaged the shop, so there is no window, and a free-form document to them fails. Meta's error codes list the failure as 131047: "More than 24 hours have passed since the recipient last replied to the sender number." Our code sent free-form documents only, kept no record of when each customer last wrote, and had no templates. The bills that reached customers can only have gone to people who had messaged their shop in the 24 hours before.

The fix is a utility template: a message approved by Meta in advance, with placeholders for the customer's name, the bill number and the amount, and the PDF as its header. Meta's template guidelines describe utility messages as ones "typically triggered by a user action or request" that "must be non-promotional", and review can take up to 24 hours. A bill sent at the counter is the user action. A line about a festival sale in the same message would turn it into marketing, which Meta prices higher and may re-categorise with a day's notice.

Accepted is not delivered

Our send route wrote status: "SENT" to a log the moment Meta's API answered with a success code, and that log was the shop's usage, meant for billing it later. Two facts make that wrong.

First, an accepted request is not a delivered message. Meta reports some failures in the API response and others only later, by webhook. Second, Meta charges per delivered message, not per request (pricing). A usage log built on API acceptance counts attempts, so it would charge a shop for bills its customers never received, and it could never show the shop which ones failed.

A webhook subscribed to message status fixes both. The log row is written as accepted when the API answers, and moves to delivered or failed when WhatsApp reports back. Only delivered counts towards usage.

Opt-in comes before the first bill

Meta's guide adds a condition before any of this: "you can only send messages to WhatsApp users who have opted in." Our code took the customer's number from the bill and sent. Nothing recorded that the customer had agreed to receive bills on WhatsApp, when, or from which shop.

In a shop that means one more field at the counter and one more column per customer: a yes, a date, and the shop it was given to. It belongs with the rest of the customer's record, scoped to the shop like every other row in the product (the same rule as in multi-tenancy in Drizzle).

The fourth gap is ours, not Meta's. To let WhatsApp fetch the PDF, the backend minted a share token for the bill, randomBytes(18) as hex, stored it on the bill, and built the link from it:

const invoiceUrl = `${origin}/api/bills/${id}/pdf?share=${shareToken}`

The PDF route skipped the login when a share parameter was present, found the bill by its token, and served it only if that bill's id matched the one in the path, so one token could never open another bill. But the token had no expiry and no way to be revoked, and it was reused for every send of the same bill. A customer who forwarded the message forwarded the bill, to anyone, for as long as the bill existed.

Meta fetches a document link once and caches the file for ten minutes, so the link only needs to live long enough for that fetch and for the customer to open it. A token that expires in days, and a way to cut it off early, is the same problem as revoking a login, which we wrote up in how to revoke a JWT. When the feature left the product, the share-token branch left with it, and that also closed every link already sent: with nothing left to mint a token, the branch served only old links.

What WhatsApp messages cost in India now

WhatsApp Business Platform pricing is per delivered message, by category, and Meta says plainly that it does "not charge for messages sent", only delivered ones, which is why the usage log above has to wait for the webhook. Meta's pricing page links a rate card in rupees, effective 1 October 2026. India's row, read on 8 November 2026:

CategoryPer delivered message, India
Marketing₹0.8631
Utility₹0.115
Authentication₹0.115
Service₹0.115, after 1,000 free a month per business number

A bill sent as a utility template costs ₹0.115 at Meta's list rate, so a shop that sends 1,000 bills in a month pays Meta about ₹115 for them. Three changes took effect on 1 October 2026, and several guides that rank for this still describe the older, conversation-based pricing:

  • Utility messages inside an open window are charged. A bill sent to a customer who had just messaged the shop used to be free. It now costs the same as any other utility message.
  • Service messages are charged after the first 1,000 a month for each business number, and without a payment method Meta stops delivering them once the free tier is used.
  • Accounts billed in India move to rupees. Meta launched INR billing on 1 January 2026 and requires it by 31 December 2026. From 1 January 2027 it stops delivering messages for accounts still billed in another currency.

Meta's own marketing page at whatsappbusiness.com still says it does not charge for utility messages sent in response to users; the developer documentation's pricing page, updated for October 2026, says otherwise. A provider that resells access adds its own price on top: Interakt, for example, lists a Growth plan at ₹2,799 a month and utility messages at ₹0.14 to ₹0.16 depending on the plan (read 8 November 2026).

What you need before the first message

Meta's Cloud API get-started guide lists the pieces: a Meta developer account, a business app, a business portfolio, and a permanent system-user access token. The business number must belong to the business, receive SMS or voice calls for verification, and not already be in use on the WhatsApp app. A new business portfolio can message 250 unique customers in 24 hours outside a service window; business verification, or 2,000 delivered messages in 30 days, raises that limit.

For a multi-tenant product the split we used still holds: the platform owns the app and the token, each shop owns its number. What we did not build, and what an integration for many shops needs, is the onboarding that gets each shop's number registered and its Phone Number ID into the product without the shop owner copying it from Meta's dashboard.

Why WhatsApp billing is now built on request

Easinvy Retail was cut back on 9 September 2026 to what it sells to every shop: bills, stock, customers, purchases, staff and reports. WhatsApp billing left the product that day and is now set up for a retailer who asks for it, with the four changes above: a utility template for bills, a status webhook that decides what counts as delivered, an opt-in recorded per customer per shop, and a PDF link that expires. If your shop or product needs it, that is what our API and integration work builds.

Questions people ask

Is WhatsApp API integration free?
No. Meta charges per delivered message on the WhatsApp Business Platform. In India, from 1 October 2026, a utility message such as a bill costs ₹0.115 at Meta's list rate, the first 1,000 service messages a month per business number are free, and Meta's test resources need no payment method.
Why is the WhatsApp API so expensive?
Meta prices each message by category, and marketing costs most: in India ₹0.8631 per delivered message against ₹0.115 for utility and authentication, effective 1 October 2026. Providers that resell access add a monthly plan and their own per-message rate on top of Meta's, so two quotes for the same messages can differ a lot.
Is there a WhatsApp API available in India?
Yes. India is a priced market on Meta's WhatsApp Business Platform rate card, with rates in rupees. Businesses billed in India must move their accounts to INR billing by 31 December 2026, and from 1 January 2027 Meta stops delivering messages for accounts still billed in another currency.

Sources

Ridam Agrawal

Founder and engineer at Easinvy. Writes up the parts of building and running a multi-tenant product that were expensive to get wrong. More about the studio →

Keep reading