MainSDK
Search⌘K
Integrations

WhatsApp

Use the WhatsApp Cloud API for MainSDK notifications and WhatsApp sign-in - Meta business setup, templates, tokens, webhooks, main.yaml and swapping.

1 interface1 driverWritten guide
Updated 2026-10-28

The WhatsApp Cloud API is Meta's hosted API for WhatsApp Business. In a MainSDK app it is the default WhatsApp channel driver for notify, so an order update or a payment reminder can reach a person on WhatsApp, and it is the default sender for WhatsApp one-time codes in auth. Pick it when your users live on WhatsApp, as many do in India, and you want to connect to Meta directly rather than through a messaging provider such as Bird or MSG91.

Interfaces it serves

Pick WhatsApp in main.yaml for any of these. Your calls stay the same, and you can swap later.

Before you start

You need a Meta business account and a phone number that is not already registered on another WhatsApp account.

  1. Set up WhatsApp Business. In Meta's developer tools, create an app with the WhatsApp product. It links to a WhatsApp Business Account owned by your business. Meta may ask you to verify the business before you can send at volume or display your business name.
  2. Add a phone number. Register the number your app sends from in the WhatsApp Business Account and note its phone number ID. Meta also gives you a test number for development, which can send to a few recipients you register.
  3. Create message templates. Business-initiated messages, sent outside the window that opens when a person messages you, must use a template Meta has approved. Create one template per notification (for example order_shipped), in each language you send, and wait for approval. Sign-in codes use a template in Meta's authentication category.
  4. Create an access token. The temporary token Meta shows during setup expires quickly. For stage and production, create a long-lived token for a system user with access to the WhatsApp Business Account.
  5. Webhook (optional). Meta sends delivery status and incoming messages to a webhook URL. The notify WhatsApp driver does not read them: it records the provider's message id when Meta accepts the message, and nothing after.

Store the token in your secrets manager, never in code or a committed env file.

Configure

For notifications, name the driver on the whatsapp channel:

main.yaml
notify:
  channels: [whatsapp, sms, email]
  whatsapp: whatsapp-cloud
  whatsapp-cloud:
    phone-number-id: "1234567890"       # the number your app sends from
    token: secret:WHATSAPP_TOKEN        # a system user's access token; this is the default name
    version: v21.0                      # the Graph API version; this is the default
  templates:
    order.shipped:
      whatsapp: { name: order_shipped, language: en, params: [orderId] }
  • name is the template name Meta approved. language is its language code (default en; a locale on the call or the person overrides it). params lists the data keys, in order, that fill the template's body variables.
  • A notification with no whatsapp template is skipped on WhatsApp with the reason no-template.
  • A literal token fails validation with MAIN-CONFIG-0004.

What the driver sends

Each message is one POST https://graph.facebook.com/<version>/<phone-number-id>/messages with Authorization: Bearer <token>. The JSON body holds messaging_product: "whatsapp", to (digits, no plus) and type: "template" with the template name, the language code and the body parameters in order. The id Meta returns becomes the delivery's providerId. The call goes through MainSDK's network layer, is never retried by it, does not follow redirects, and reads at most 64 KiB of the answer. A 429, a 5xx or no answer is a retryable MAIN-NOTIFY-0007 and notify tries the next channel.

For sign-in, add whatsapp to the methods and pick the sender:

main.yaml
auth:
  driver: github
  methods: [passkeys, whatsapp]
  whatsapp: { driver: whatsapp-cloud }

India users usually expect WhatsApp first. Put it first in the market overlay:

main.in.yaml
notify:
  channels: [whatsapp, sms, push, email]
auth:
  methods: [whatsapp, passkeys]

Use it

Notify a person. WhatsApp is used when it is the first channel they allow and they have recorded consent; the template name maps to your approved template.

app/orders/ship.ts
import { notify } from "mainsdk";

export async function orderShipped(user: { id: string }, orderId: string) {
  return notify(user, {
    template: "order.shipped",
    data: { orderId },
    category: "orders",
    idempotencyKey: `order.shipped:${orderId}`,
  });
}

Sign in with a WhatsApp one-time code:

app/sign-in/whatsapp/actions.ts
"use server";
import { auth } from "mainsdk";

export async function sendCode(phone: string) {
  const { challengeId } = await auth.whatsapp.send(phone);
  return challengeId;
}

export async function confirmCode(challengeId: string, code: string) {
  return auth.whatsapp.verify(challengeId, code);   // MAIN-AUTH-0006 on reuse, expiry or too many tries
}

The MainSDK template name (order.shipped) maps to the Meta template name and language through notify.templates.<name>.whatsapp.

Local development and testing

  • No Meta account: with notify.whatsapp: local, mainsdk dev logs one line per WhatsApp message with the number masked.
  • Meta test number: send real messages to the few recipients you register on it, with a temporary token, before your own number and templates are approved.
  • Tests: createTestApp() from @mainsdk/testing runs notify on a local channel when notify.whatsapp is local; read it with channels(app).whatsapp. It enforces consent, opt-outs and quiet hours. channels(app).whatsapp?.failNext(1) simulates an outage, and fakeMessagingServer("whatsapp-cloud") records the request the real driver makes.

What MainSDK adds on top

  • WhatsApp only to recipients with recorded consent; without it the channel is skipped with MAIN-NOTIFY-0004 and the next channel is tried.
  • Business-initiated messages only with provider-approved templates.
  • Opt-outs and quiet hours respected, in the person's time zone.
  • Sign-in codes single use, expiring after 5 minutes, closed after 5 wrong tries, and rate limited per phone and per IP.
  • A retry with the same idempotency key never sends twice.

See Notifications and Sign-in and sessions.

Limits and notes

  • The WhatsApp channel driver is built (spec email-notify). The auth.whatsapp sender comes with the auth spec.
  • The driver declares its own host, graph.facebook.com, so network.allow does not change.
  • Set notify.limits.whatsapp-per-day to bound cost; more is skipped with MAIN-NOTIFY-0010.
  • Meta limits how many people a new number can message in a day and raises the limit as quality holds. Plan rollouts with notify.rollout stages.
  • Phone numbers are personal data. Label the fields that store them personal in db.
  • Consent records are kept in your app's kv store, per phone number, as a keyed hash. Record consent with notify.consent.create(user, "whatsapp").

Swap to or from it

main.yaml
notify:
  whatsapp: bird   # was whatsapp-cloud
auth:
  whatsapp: { driver: bird }   # was whatsapp-cloud

Your notify() and auth.whatsapp calls stay the same, and the WhatsApp conformance cases pass for WhatsApp Cloud API, Bird and MSG91. Add the notify.bird block with it (see Bird).

FAQ

Can I send any text on WhatsApp? Not as the first message. Business-initiated messages must use an approved template, and MainSDK sends only to people with recorded consent.

Do I need a separate setup for sign-in codes? The same WhatsApp Business Account works for both. Sign-in codes need an approved template in the authentication category.

WhatsApp Cloud API or Bird or MSG91? All three pass the same conformance cases. Use the Cloud API to deal with Meta directly; use Bird or MSG91 if you already send SMS through them.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet