GuideArchitecture

WhatsApp coexistence with official templates and wuapi replies

One WhatsApp number on the official Cloud API through coexistence, with wuapi linked as a companion to answer. The architecture, the math and the code.

wuapiGuides7 min read
One number with two sides: the official Cloud API side sends an approved template, the customer answers in the same chat, and wuapi, linked to the number as a companion, holds the conversation with no fee per reply.[CLOUD API SIDE]YOUR NUMBERapproved templaterestock alertsame chatCUSTOMERanswers[WUAPI COMPANION]YOUR NUMBER, RELINKEDmessagereplymessagereplyno fee per replyfee per template[GUIDES]TEMPLATES OPEN IT, WUAPI HOLDS IT

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.

  1. 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.
  2. 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,000 free 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.

Your backend
Cloud API side
Customer
wuapi companion
Decides to reach outopt-in on record
Sends an approved templatebilled per template
Sees it as the number's own messagemessage.sent · source: phone
Replies in the same chat
Receives the replymessage.received
Answers once, from one feedPOST /v1/messages
Sends from the app side, no per-message feemessage.delivered
end

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 factwhat the docs say
companion devices at onboardingevery companion is unlinked
linking a companion againsupported, except WhatsApp for Windows and WearOS
messages sent from the app sidefree, and outside the API's service window
messages sent through the Cloud APIbilled at the Cloud API rates
messages sent and receivedmirrored between the Cloud API and the app
throughput on the API sidefixed at 20 messages per second
groups, calls and channels on the API sidenot supported
disappearing, view once, live location, broadcast liststurned off after onboarding
chat historyup to 6 months can be synced to the API side
phone inactive for about 14 daysthe number disconnects
a companion inactive for about 30 daysit 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.

lineall on the official APIcoexistence with wuapi
2,000 marketing templates at $0.0625$125$125
15,000 replies at $0.0068, 1,000 free$95.20sent from the app side, no fee
the number on wuapi, with 0.5 GB of proxy past the included 0.5 GBnone$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.

lineall on the official APIofficial API plus wuapi
20,000 marketing templates at $0.0625$1,250$1,250
200,000 replies at $0.0068, 1,000 free$1,353through wuapi
14 linked numbers, 7 GB of proxy past the included 7 GBnone$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.

  1. 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.
  2. That page picks the customer's number, sticky, and redirects to https://wa.me/<number>?text=... with a short reference in the prefilled text.
  3. 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.

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.sent with source: phone and direction: outbound. Your own sends through wuapi carry source: api.
  • On the Cloud API, messages sent from the app or a supported companion arrive on the smb_message_echoes webhook 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.

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 500 sends a day, one number holds 15,000 replies 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.

wuapi is an independent service. It is not affiliated with, endorsed or sponsored by WhatsApp. WhatsApp is a trademark of its respective owner.

03/What to read next

Try it

Link a number and send your first message.

One REST call, a typed SDK, and webhooks signed with HMAC-SHA256 over the raw body. No per-message fees.

The whole API is in the docs, and the docs are in one file if you are handing the work to a coding agent.

Guide: keep your number healthy · openapi.json