Guides

WhatsApp Business

AdminUpdated Sep 19, 2026

WhatsApp Business

Cloud API setup, the 24-hour session window, message templates, opt-in compliance.

WhatsApp is a strictly-regulated business channel: you can't message a customer until they message you (or have opted in via another channel), and once a conversation is idle for 24 hours you can only send pre-approved templates. Chatly enforces the window on the server so you don't get your number rate-limited or banned.

We use Meta's Cloud API (the modern, hosted-by-Meta API — not the deprecated On-Premise variant). You bring your own Meta Business Account, your own WhatsApp Business Account (WABA), and your own number. We never touch the underlying phone line; we just speak HTTPS to Meta on your behalf, against Graph v23.0.

Info — No Twilio / 360dialog / MessageBird middleman

Chatly talks directly to Meta's Cloud API. No reseller markup, no mystery latency. Your WABA stays yours; you keep it if you ever leave.

Two ways to connect

There are two onboarding paths, and which one you see depends on the deployment you are using — not on your plan.

Embedded signup (one click). If the operator of your Chatly deployment has registered a Meta app and set META_APP_ID, META_APP_SECRET and META_WHATSAPP_CONFIG_ID, the WhatsApp install dialog shows a Continue with Facebook button. You sign in, pick or create a WhatsApp Business Account, and that is the whole of it: the phone number, the access token, the webhook subscription and the per-number callback are all configured for you. Nothing is copied and pasted. Chatly's own Meta app is the party onboarding you; your WABA and your number remain yours, and you can move them to your own app at any time (see below).

Bring your own Meta app. Register your own Meta app, mint your own system-user token and paste five values into the form. This is always available — it is one link away even where embedded signup is offered, and it is the only path on a self-hosted install whose operator has not registered a Meta app. Everything from "24-hour window" onwards in this page applies identically to both.

Info — Nothing half-works

Embedded signup is offered only when the server confirms all three of META_APP_ID, META_APP_SECRET and META_WHATSAPP_CONFIG_ID are set. With any of them missing the button is not rendered at all and you get the bring-your-own-app form — see environment variables if you are the operator.

The two-step verification PIN

Registering a number on the WhatsApp Cloud API sets its two-step verification PIN — six digits. For a new number, Chatly generates one, sends it to Meta and keeps it on the channel (encrypted, and never shown back to you), so the number can be re-registered later without you holding anything.

There is one case where it has to come from you: a number moving here from another WhatsApp provider already carries a PIN that only you know, and Meta refuses to activate the number for any other value (error 133005). The install dialog and the Reconnect with Facebook card each have an optional PIN field for exactly that. Leave it blank for a new number.

If you ever change the PIN in WhatsApp Manager, put the new one in that field the next time you reconnect — otherwise activation fails and the number cannot send or receive.

Releasing a number

Disabling a channel is a pause. Releasing a number is not.

A WhatsApp number stays registered to the app that onboarded it. Until Chatly deregisters it, Meta answers "already in a partner app" to any attempt to add that number anywhere — including back here, and including at a different provider.

Disabling a channel does not release the number, on purpose: disable is reversible in one click, and a released number leaves the Cloud API entirely and can be claimed by someone else while you think it is merely paused.

When you are genuinely finished with a number, open the channel and use Release this number (on the Connection card, for channels connected through embedded signup). It clears the webhook override, deregisters the number at Meta, drops the WhatsApp Business Account subscription if no other channel here still uses it, and disables the channel. Conversations and history are kept — nothing is deleted. The button is not offered for a bring-your-own-app channel: that number is registered to your Meta app, and deregistering it is yours to do in WhatsApp Manager.

Moving between the two

A channel created by embedded signup can be moved onto your own Meta app without losing its conversation history: edit the channel and paste your own appSecret (and, if you are also rotating the credential, your own accessToken). Chatly keeps using whatever appSecret is stored on the channel, so the moment yours is there it is yours that verifies the webhooks. Re-running the sign-up on an existing channel is the same edit in reverse.

Setup

1. WhatsApp Business Account (Meta side)

Skip this whole section if you used embedded signup — it did all of it.

  1. In Meta Business Manager, create (or claim) a WhatsApp Business Account and add a phone number you own. SMS or voice OTP verifies ownership.

  2. Add your business profile (display name, category, address, hours, website).

  3. Display name has to be approved by Meta — takes minutes to hours.

  4. Generate a system user access token with the whatsapp_business_messaging + whatsapp_business_management permissions. Set the expiry to Never.

  5. Decide on a webhook verify token — any random string you choose. You type the same value into Chatly and into Meta.

2. Add the channel in Chatly

Dashboard

  1. Channels → New → WhatsApp.

  2. Paste all five required fields:

    • WhatsApp Business Account ID (wabaId)

    • Phone number ID (phoneNumberId)

    • Access token (accessToken — the system user token)

    • Webhook verify token (webhookVerifyToken — the string you chose)

    • App secret (appSecret, from your Meta App → Settings → Basic)

  3. Save.

API

POST /v1/channels — Bearer token

POST /v1/channels
Authorization: Bearer …
Idempotency-Key: 9c02…
Content-Type: application/json

{
  "type": "whatsapp",
  "name": "WhatsApp — Support",
  "publicId": "acme-whatsapp",
  "config": {
    "wabaId": "0987654321",
    "phoneNumberId": "1234567890",
    "accessToken": "EAAB...",
    "webhookVerifyToken": "a-random-string-you-choose",
    "appSecret": "abc123..."
  }
}

Warning — Saving does not test your credentials

Creating a WhatsApp channel writes the row and returns it. Nothing calls Meta to validate the token, the WABA id or the phone number id — the first thing that exercises them is Meta's verification handshake, and then real traffic. A wrong appSecret in particular looks perfectly healthy right up until every inbound message is rejected.

Warning — publicId is required and you choose it

POST /v1/channels rejects a body without publicId (1–64 chars, unique across the deployment). It is immutable after creation because it appears in your webhook URL.

3. Point Meta's webhook at us

There is one ingest route for every channel, keyed by the channel's publicId:

GET /v1/channels/webhook/{publicId} — Public

POST /v1/channels/webhook/{publicId} — Public

In Meta → your App → WhatsApp → Configuration → Webhook:

Parameters

Name

Type

Description

Callback URL (required)

url

https://api.chatlychat.com/v1/channels/webhook/{publicId} — the channel detail screen prints the exact URL for copy-paste.

Verify token (required)

string

The same string you put in webhookVerifyToken. Meta GETs the callback URL with hub.mode=subscribe and refuses to save it unless we echo hub.challenge back. Compared in constant time; falls back to the deployment-wide META_VERIFY_TOKEN when the channel has none.

Subscribed fields (required)

enum[]

messages. Other fields can be subscribed at Meta but are not parsed today.

We verify the X-Hub-Signature-256 header against your appSecret on every inbound POST, over the raw request bytes rather than a re-serialised body. A bad signature is rejected with 403; an unknown publicId, a disabled channel, or a bad verify token is a 404/403 that deliberately reveals nothing about which half was wrong.

The 24-hour session window

WhatsApp's business policy: after a customer sends you a message, you can free-form reply for 24 hours. After that, the only way to reach them is a pre-approved template.

Warning — Free-form outside the window is refused before it reaches Meta

The adapter refuses the send with the delivery reason whatsapp_outside_24h_window, and the agent UI surfaces it with a "send a template instead" action. Nothing is billed and nothing reaches Meta.

How the window works in Chatly:

  1. Inbound customer message → window opens; expires at inbound_at + 24h.

  2. Every subsequent inbound message extends the window.

  3. Agent or AI sends free-form text → allowed if now() < expires_at.

  4. Outside the window a send is only accepted when it carries a template binding.

Message templates (HSMs)

Templates are authored and submitted for review in Meta's own UI (WhatsApp Manager → Manage Templates). Chatly reads them back; it does not create, edit or submit them.

GET /v1/channels/{id}/whatsapp/templates — Bearer token

The list is fetched live from Graph using the channel's wabaId + accessToken and is filtered to APPROVED templates only — Meta rejects a send against anything else, so offering a PENDING template would hand an agent a reply that cannot go out.

Fields returned per template

Name

Type

Description

name

string

Template name as approved in Meta Business.

language

string

e.g. en_US. One template per language.

category

string

UTILITY, MARKETING or AUTHENTICATION. Defaults to UTILITY when Meta omits it.

body

string

The BODY component text, with its {{1}} placeholders left in for preview.

parameterCount

integer

The highest placeholder index, not the number of matches — a body using {{1}} twice still takes one parameter.

Sending a template

A template is sent as an ordinary message carrying a metadata.whatsappTemplate object. The worker's dispatcher lifts it back out and hands it to Meta; the message body you send is what gets stored in the transcript, so write the rendered text there rather than the template name.

POST /v1/conversations/{id}/messages — Bearer token

POST /v1/conversations/01H7.../messages
Authorization: Bearer ck_…
Idempotency-Key: 7ba1…
Content-Type: application/json

{
  "body": "Hi Jamie, your order ORD-1234 just shipped.",
  "metadata": {
    "whatsappTemplate": {
      "name": "order_shipped",
      "language": "en_US",
      "params": ["Jamie", "ORD-1234"]
    }
  }
}

Info — The Node SDK cannot express this yet

@livechat/sdk-node's conversations.send() types only conversationId, body and kind — there is no metadata field on MessageInput. Send templates with a plain HTTP request until it gains one.

A whatsappTemplate missing name or language is discarded before the Graph call, and the send is then treated as free-form — which outside the window means it is refused.

Info — Authoring happens at Meta

There is no create-template endpoint and no template builder in the dashboard. Write the template in WhatsApp Manager, wait for approval, then it appears in the picker and in the list endpoint above.

Media + interactive messages

Beyond plain text, the adapter maps rich payloads onto Cloud API types:

  • Images, video, audio, documents — sent by link, with optional caption and (for documents) filename. Declared capabilities are image, video, audio and document.

  • Interactive: Buttons — up to 3 quick replies, labels capped at 20 characters.

  • Interactive: List — a carousel payload degrades to a single-section list message.

On the inbound side the adapter reads text, media — image, video, audio, document and sticker, surfacing the Graph media id an agent needs to fetch the file — location pins, and taps on our own interactive buttons and list rows.

Emoji reactions and contact cards (vCard) are not parsed inbound and cannot be sent.

Opt-in compliance

Meta requires explicit opt-in before a business-initiated template to someone who has not messaged you. Chatly records this on contact_channel_consent, one row per (contact, channel):

Parameters

Name

Type

Description

channel

enum

email | sms | whatsapp.

state

enum

granted or revoked. For WhatsApp no row means refused — the opposite default to email.

source

string

Where the opt-in came from — widget_form, checkout, import, api, agent, reply_stop, …

evidence

text

What you would show a regulator. "Checkout checkbox, 2026-03-04, submission a1b2c3" — not "the row was there".

grantedAt / revokedAt

timestamp

A revoked row is kept rather than deleted; proving someone opted out matters as much as proving they opted in.

Warning — Consent is enforced on campaigns, not on every send

The recipient resolver for campaigns drops WhatsApp contacts without a granted row. An agent reply inside an open conversation is not gated on consent — the 24-hour window is what constrains that path.

Quality + delivery

Meta computes a quality rating for your number from block rate, complaint rate and response time, and moves you between messaging tiers (250 → 1,000 → 10,000 → 100,000 → unlimited).

Warning — Chatly does not surface your quality rating

The phone_number_quality_update webhook field is not parsed, so tier changes and quality drops will not show up in the dashboard. Watch them in WhatsApp Manager. Delivery-side failures on the channel are visible at GET /v1/channels/{id}/delivery-failures.

Costs

You pay Meta directly. Chatly does not mark up.

  • UTILITY conversation: $0.005–$0.04 depending on country.

  • MARKETING conversation: $0.02–$0.15 depending on country.

  • AUTHENTICATION: priced like UTILITY in most countries.

A "conversation" is a 24-hour window of messaging, not a single message — keeping conversations open is cheap. Check Meta's current rate card before you budget; these bands move.

Troubleshooting

Warning — Meta refuses to save the callback URL

The GET handshake failed. Either the channel is disabled, the publicId in the URL is wrong, or the verify token you typed into Meta does not match webhookVerifyToken on the channel. All three answer the same way on purpose.

Warning — Webhook signature failures (403 on every inbound)

The appSecret you stored doesn't match the one in your Meta App settings. Re-copy from Meta App → Settings → Basic (it's hidden by default; click the eye icon). Note that saving the channel never checked it — a wrong secret looks connected.

Info — Why a brand-new number can only message 250 people/day

You start in the lowest messaging tier (250 unique recipients per rolling 24h). You graduate by sending real conversations with green quality. Meta watches throughput + quality before raising the limit.

Was this page helpful?
WhatsApp Business