WhatsApp coexistence puts a WhatsApp Business app number on the official Cloud API while the app keeps working on the phone. That makes a split possible on one number: the Cloud API sends the approved templates that open conversations, and wuapi, linked to the same number as a companion device, receives the answers and replies. Replies sent from the app side carry no per-message fee. This guide is the architecture, the math, the code and the limits.
#Why this matters from October 1, 2026
Two rules on the official Cloud API shape the bill.
- Templates are required to start. Outside a 24-hour customer service window, which opens when the customer writes to you, a business can only send approved templates through the API.
- Replies start costing money. From October 1, 2026, free-form replies sent through the Cloud API inside that window are charged per message, at the utility rate of the customer's country, after
1,000free service messages per business number per month.
The coexistence docs add the rule this pattern is built on: messages the business sends from the WhatsApp Business app stay free, and only messages sent through the Cloud API are billed. A companion device sends as the app does. So the template goes through the API, and the conversation after it goes through the app side.
#One number, two sides
The phone keeps the WhatsApp Business app. The number is onboarded to the Cloud API through coexistence, which unlinks every companion device. Then you link wuapi again, the way you link WhatsApp Web, and it becomes the side that holds the conversation.
time runs down the page · 7 steps
One number: the Cloud API opens the chat, the wuapi companion holds it
The customer sees one chat with one business. Your backend sees two feeds of the same conversation, and answers from one of them (Step 4).
#What coexistence allows, and its limits
We read the official developer docs for coexistence on 27 September 2026. These are the facts the pattern depends on.
| coexistence fact | what the docs say |
|---|---|
| companion devices at onboarding | every companion is unlinked |
| linking a companion again | supported, except WhatsApp for Windows and WearOS |
| messages sent from the app side | free, and outside the API's service window |
| messages sent through the Cloud API | billed at the Cloud API rates |
| messages sent and received | mirrored between the Cloud API and the app |
| throughput on the API side | fixed at 20 messages per second |
| groups, calls and channels on the API side | not supported |
| disappearing, view once, live location, broadcast lists | turned off after onboarding |
| chat history | up to 6 months can be synced to the API side |
| phone inactive for about 14 days | the number disconnects |
| a companion inactive for about 30 days | it disconnects |
wuapi is neither a Windows nor a WearOS client. It links the way WhatsApp Web links. It stays connected around the clock, so the 30-day companion rule does not bite, but the phone itself has to open WhatsApp at least every two weeks.
#The worked scenario: an online store in Brazil
A store sends 2,000 marketing templates a month (restock alerts, abandoned carts, offers) to customers who opted in, and those conversations produce 15,000 replies from the store. That is what this guide plans one linked number to send: 500 a day, on the careful side. Rates are the official card in force from October 1, 2026, and the math is the same calculator as the Cloud API pricing page.
| line | all on the official API | coexistence with wuapi |
|---|---|---|
| 2,000 marketing templates at $0.0625 | $125 | $125 |
| 15,000 replies at $0.0068, 1,000 free | $95.20 | sent from the app side, no fee |
| the number on wuapi, with 0.5 GB of proxy past the included 0.5 GB | none | $6.50 |
| a month | $220 | $132 |
The store saves $88.70 a month, $1,064 a year, on the same number. The templates cost the same either way, so the whole difference is the replies: $95.20 on the official API against $6.50 for the number on wuapi, about 15×. There is no second number to buy, publish or explain to customers.
#When one number isn't enough
Throughput on wuapi is per number. A linked number is planned at 500 sends a day, and with the recommended pacing on it sends at most 12 a minute. Past what one number sends, the replies need more numbers, and the one-number pattern becomes a two-number one.
The same store at ten times the size sends 20,000 templates and 200,000 replies a month.
| line | all on the official API | official API plus wuapi |
|---|---|---|
| 20,000 marketing templates at $0.0625 | $1,250 | $1,250 |
| 200,000 replies at $0.0068, 1,000 free | $1,353 | through wuapi |
| 14 linked numbers, 7 GB of proxy past the included 7 GB | none | $71.43 |
| a month | $2,603 | $1,321 |
That saves $1,282 a month, $15,381 a year. The replies cost $1,353 on the official API against $71.43 on wuapi, about 19×, and need 14 linked numbers at the planning default: the coexistence number and 13 more. The extra numbers are ordinary WhatsApp numbers linked to wuapi. The template's button sends the customer to one of them.
- The template carries a URL button to a page on your own domain,
/chat/:token, rather than straight to a chat link, which template review guidelines have discouraged. - That page picks the customer's number, sticky, and redirects to
https://wa.me/<number>?text=...with a short reference in the prefilled text. - The customer sends that first message, so the extra number only answers people who wrote to it.
const NUMBERS = process.env.WUAPI_NUMBERS!.split(",") // "+5511900000001,+5511900000002,..."
export async function GET(_req: Request, { params }: { params: Promise<{ token: string }> }) {
const { token } = await params
const customer = await db.customers.findByToken(token)
if (!customer) return new Response("Link expired", { status: 404 })
customer.wuapiNumber ??= NUMBERS[hash(customer.phone) % NUMBERS.length]
await db.customers.save(customer)
const text = encodeURIComponent(`Hi! About my restock alert (ref ${customer.ref})`)
return Response.redirect(`https://wa.me/${customer.wuapiNumber.replace(/^\+/, "")}?text=${text}`, 302)
}Use two numbers from the start, too, if you'd rather not have the app on the number your templates come from.
#Step 1: onboard the number to coexistence
Onboarding goes through embedded signup with the provider you use for the official API. The owner of the WhatsApp Business app scans a code from the app and chooses whether to share the chat history. Nothing here is wuapi yet. Two things to check before you start:
- The app is the WhatsApp Business app, on the phone that holds the number, and it will stay in use: the number disconnects after about 14 days without it.
- Every linked device will be unlinked, WhatsApp Web included. Plan a minute to link them again.
#Step 2: link wuapi again, as a companion
Create an account and link it by pairing code or QR code, exactly as you would any number. Choose a proxy exit in the number's country.
import { Wuapi } from "@wuapidev/sdk"
const wuapi = new Wuapi({ apiKey: process.env.WUAPI_API_KEY })
const account = await wuapi.accounts.create({
name: "Store, coexistence",
proxyLocation: { country: "BR", city: "sãopaulo" },
pairingPhone: "+5511900000000",
})
// Type this code on the phone: Linked devices > Link with phone number.
const ready = await wuapi.accounts.waitUntilReady(account.id, {
onPairingCode: (code) => console.log("pairing code", code),
})
console.log(ready.status) // "ready"History import on wuapi is off by default (historySync: none), which is what you want here: the Cloud API side can already hold up to 6 months, and wuapi only needs what happens from now on. Accounts and the pairing code are in the docs.
#Step 3: send the templates through the Cloud API
The request below is the official API's template send, as a plain HTTP call. Take the endpoint, token and exact field names from your official provider's docs for your API version.
// OFFICIAL_MESSAGES_URL: the messages endpoint of your number, from your official provider.
export async function sendOpener(customer: { phone: string; name: string }) {
const res = await fetch(process.env.OFFICIAL_MESSAGES_URL!, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OFFICIAL_ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
messaging_product: "whatsapp",
to: customer.phone.replace(/^\+/, ""),
type: "template",
template: {
name: "restock_alert",
language: { code: "pt_BR" },
components: [{ type: "body", parameters: [{ type: "text", text: customer.name }] }],
},
}),
})
if (!res.ok) throw new Error(`template send failed: ${res.status}`)
}The template needs no button to reach wuapi: the customer answers in the same chat, and the companion receives it.
#Step 4: answer from wuapi, and only from wuapi
The docs say messages are mirrored between the Cloud API and the app. So the customer's reply arrives twice: on the Cloud API's messages webhook and on wuapi's message.received. Each side also sees what the other sent:
- On wuapi, a message the number sent from anywhere else, templates from the API included, arrives as
message.sentwithsource: phoneanddirection: outbound. Your own sends through wuapi carrysource: api. - On the Cloud API, messages sent from the app or a supported companion arrive on the
smb_message_echoeswebhook field. Whether wuapi's sends are echoed there is part of what our pilot is checking.
The two feeds give the same message different ids, so don't try to match them one to one. Pick one feed to act on: wuapi's message.received. Store everything else, react to none of it.
import { createHmac, timingSafeEqual } from "node:crypto"
import { verifyWebhook } from "@wuapidev/sdk"
// wuapi: the companion on the coexistence number. The only feed that answers.
export async function wuapiRoute(req: Request) {
const raw = await req.text()
const event = await verifyWebhook(raw, req.headers.get("wuapi-signature"), process.env.WUAPI_WEBHOOK_SECRET!)
if (event.type === "message.received") await answer(event.data.object)
if (event.type === "message.sent" && event.data.object.source === "phone") {
await db.messages.saveOutbound(event.data.object) // a template from the API, or the phone
}
return new Response(null, { status: 204 })
}
// Official: the same number seen from the Cloud API. Stored, never answered.
export async function officialRoute(req: Request) {
const raw = await req.text()
const expected = "sha256=" + createHmac("sha256", process.env.OFFICIAL_APP_SECRET!).update(raw).digest("hex")
const got = req.headers.get("x-hub-signature-256") ?? ""
if (got.length !== expected.length || !timingSafeEqual(Buffer.from(got), Buffer.from(expected))) {
return new Response("invalid signature", { status: 401 })
}
for (const entry of JSON.parse(raw).entry ?? []) {
for (const change of entry.changes ?? []) {
// "messages": the same inbound wuapi already has. "smb_message_echoes": sends from the app side.
await db.officialEvents.save({ field: change.field, value: change.value })
}
}
return new Response(null, { status: 200 })
}const wuapi = new Wuapi({ apiKey: process.env.WUAPI_API_KEY })
async function answer(m: { id: string; accountId: string; from: string; text: string | null }) {
const customer = await db.customers.findOrCreate({ contactId: m.from })
await db.messages.saveInbound(customer.id, m)
const reply = await compose(customer, m.text ?? "") // your team, your inbox or your agent
await wuapi.messages.send(
{ accountId: m.accountId, to: m.from, text: reply },
{ idempotencyKey: `reply-to-${m.id}` },
)
}wuapi delivers each event at least once, so the same message.received can arrive twice. The Idempotency-Key built from the inbound message's id makes the second reply a replay of the first: it goes out once. Webhooks covers the signature and the delivery schedule.
#Step 5: one customer record
Key the customer by their phone number in E.164, the form wuapi uses (+5511...). The Cloud API side always gives you the number. wuapi gives you the number too, or a lid: id when the customer hides it, so store that id on the customer the first time you see it next to their number. Contacts explains those ids.
Keep every message from both feeds in your own database, attached to the customer. Neither side's history is the source of truth: yours is.
#Opt-in and consent
The official platform's business messaging policy says you may contact people only if they gave you their number and you received their opt-in to hear from you. That covers every template, and it is your job to record it: where they opted in, when, and for what.
- Record opt-in per customer with its source and date, before the first template.
- Honor a stop from either side. It is one number and one customer.
- Don't open conversations from wuapi. Anything that starts with you goes through the Cloud API as a template. wuapi answers people who wrote.
- Keep the number healthy the way keeping a number healthy describes. It is the same number your templates come from.
#Pitfalls
- Answering from both feeds. The reply arrives on both sides. Answer from wuapi only.
- Forgetting the phone. About 14 days without the WhatsApp Business app open and the number disconnects, companions and all.
- Groups on the API side. Coexistence has none there. A group conversation on this number belongs to the app side, which means wuapi.
- Planning past one number. At
500sends a day, one number holds15,000replies a month. Plan the extra numbers before the volume arrives. - Moving traffic before the pilot. Link wuapi to one coexistence number, run a week of real conversations, then move the rest.
- No fallback. Any number can be restricted. Your backend can still reach an opted-in customer with a template while you sort it out.
#Questions people ask
Can a WhatsApp coexistence number also be linked to wuapi?
The docs say companions can be linked again after coexistence onboarding, except WhatsApp for Windows and WearOS, and wuapi links the way WhatsApp Web does. We are testing a wuapi session on a coexistence number now, so run a pilot on one number first.
Are replies sent through wuapi free on a coexistence number?
The coexistence docs say messages the business sends from the WhatsApp Business app stay free and only Cloud API messages are billed. A reply through wuapi goes out from a companion on the app side. wuapi bills the number, never the message.
Why does my backend get every customer message twice?
Coexistence mirrors messages between the Cloud API and the app, so a reply arrives on the Cloud API webhook and on wuapi's message.received. Answer from wuapi only, store the other feed, and use an Idempotency-Key per inbound message.
How many replies can one number send?
This guide plans 500 a day per linked number, which is 15,000 a month, on the careful side. With the recommended pacing on, a number sends at most 12 a minute. Past that, add numbers and send customers to them with the template's button.
Do I still need templates?
Yes, to start a conversation with someone who has not written to you in the last 24 hours. Templates go through the Cloud API and are billed there. wuapi is for the conversation after the customer answers.
#Where to go next
The Cloud API pricing page runs this math for your own country and mix, and wuapi pricing has the tiers. The docs cover linking, sending and webhooks, and linked device or Cloud API is the longer version of which model fits which job.