# Genuka WA — full documentation > Genuka WA is a REST API for the official WhatsApp Business Platform (Meta's Cloud API). Genuka is a Meta Tech Provider: you connect your own WhatsApp Business number through Meta's Embedded Signup, then send notifications, one-time codes and campaigns over HTTP and receive replies and delivery statuses on signed webhooks, without applying to Meta as a provider yourself. Meta's message rates are billed by Meta with no markup from Genuka; Genuka charges a subscription per WhatsApp number. Index: https://wa.genuka.com/llms.txt --- # WhatsApp Business API documentation URL: https://wa.genuka.com/en/docs Language: English > The official WhatsApp Business Platform through a REST API — without applying to Meta yourself. Genuka WA gives you access to **Meta's official WhatsApp Business Platform** through a plain REST API. No Meta developer account to create, no provider application to file with Meta, no access token to rotate: Genuka is a Meta Tech Provider and carries that complexity for you. Meta business verification is not required to get started; in practice it is what unlocks OTP codes and higher sending limits ([details](https://wa.genuka.com/en/docs/guides/without-meta-verification)). ## What you can do * [Send messages](https://wa.genuka.com/en/docs/messages): Text, media, buttons, lists, forms — plus approved templates to reach a customer outside the 24-hour window. * [Receive events](https://wa.genuka.com/en/docs/webhooks): Inbound messages, delivery and read receipts, template status changes, marketing opt-outs. * [Automate campaigns](https://wa.genuka.com/en/docs/campaigns): Broadcast a template to a contact list and track every send through to the read receipt. * [Integrate in TypeScript](https://wa.genuka.com/en/docs/library): `@genuka/whatsapp` ships typed builders that reject invalid payloads before the network call. ## Two rules to know before you start WhatsApp is not a free-for-all sending channel. Two constraints shape every integration, and it is cheaper to meet them here than in your production logs. > [!WARNING] > **The 24-hour service window** > > You may send a free-form message — text, image, buttons — only within **24 hours** of the last > message you received from that contact. After that, only a **template approved** by Meta will be > delivered. Anything else fails with error `131047`. > [!NOTE] > **Opt-in is mandatory** > > You may only write to people who agreed to be contacted. A user can opt out of marketing > messages at any time; you are notified by webhook and sending must stop immediately. ## Where to start New to the platform? Follow the [quickstart](https://wa.genuka.com/en/docs/quickstart): account, connected number, first message, in about ten minutes. Integrating from existing code? Go straight to [authentication](https://wa.genuka.com/en/docs/authentication), then [sending messages](https://wa.genuka.com/en/docs/messages). ## Guides, comparisons and error codes * [Guides](https://wa.genuka.com/en/docs/guides): One use case per page: OTP codes, order notifications, campaigns, webhooks, coexistence, Meta pricing in Africa. * [Comparisons](https://wa.genuka.com/en/docs/compare): Genuka WA against Twilio, 360dialog, WATI and unofficial WhatsApp APIs. * [Error codes](https://wa.genuka.com/en/docs/errors): Meta's error codes (131047, 131026, 130429…), one per page: what each means and whether to retry. ## For AI agents and tools * **MCP server**: `@genuka/whatsapp-mcp` gives Claude, Cursor or VS Code access to your numbers, templates and campaigns. Setup in [WhatsApp API for AI agents](https://wa.genuka.com/en/docs/guides/ai-agents). * **[/llms.txt](https://wa.genuka.com/llms.txt)**: an index of the whole documentation for language models, and [/llms-full.txt](https://wa.genuka.com/llms-full.txt) for every page in a single file. * **Markdown of any page**: append `.mdx` to its URL, for example [/en/docs/quickstart.mdx](https://wa.genuka.com/en/docs/quickstart.mdx). * **[/openapi.json](https://wa.genuka.com/openapi.json)**: the REST API described in OpenAPI 3.1. * **[/pricing.md](https://wa.genuka.com/pricing.md)**: plans and their limits, in Markdown. --- # Quickstart URL: https://wa.genuka.com/en/docs/quickstart Language: English > From sign-up to your first message, in about ten minutes. 1. ### Create your account Sign up with your business name, an email and a password. Your workspace opens on a 7-day free trial: one WhatsApp number, its full message allowance, REST API and webhooks included. No card required. 2. ### Connect your WhatsApp number From your dashboard, open **Connections**. You will find a unique URL there. Open it to connect your own number through Meta's Embedded Signup — or send it to a business whose numbers you manage. The account, its numbers and its templates then appear in your workspace automatically. > [!NOTE] > **Already using the WhatsApp Business app?** > > You can keep the app on your phone while plugging the API into the same number: that is > **coexistence** mode. Your conversations stay in sync both ways. The one difference to know: > send throughput is capped at 20 messages per second. 3. ### Create an API key Still in the dashboard, under **Developers**, generate a key. It is shown **once** — copy it immediately. 4. ### Send your first message First, look up the connection you want to send from: ```bash curl https://wa.genuka.com/api/v1/connections \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" ``` Then send: ```bash curl https://wa.genuka.com/api/v1/messages \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "cnx_...", "to": "+237699001122", "text": "Hello from Genuka WA 👋" }' ``` > [!WARNING] > This first send only works if the recipient messaged you within the last **24 hours**. > Otherwise you need an approved template — see [Sending messages](https://wa.genuka.com/en/docs/messages). 5. ### Receive replies In the portal, under **Webhooks**, register your application's URL and pick the events you want. The signing secret is shown **once** — copy it. Every delivery is signed; your endpoint must verify that signature before processing anything. See [Webhooks](https://wa.genuka.com/en/docs/webhooks). ## Next * [Authentication](https://wa.genuka.com/en/docs/authentication) — API keys, scope, rotation. * [Sending messages](https://wa.genuka.com/en/docs/messages) — every available type and the 24-hour rule. * [Webhooks](https://wa.genuka.com/en/docs/webhooks) — events, signature, replay. --- # Connecting a number URL: https://wa.genuka.com/en/docs/onboarding Language: English > Numbers join through Meta's Embedded Signup — no credentials ever pass through us, and none through you. ## The connect link Every account has a unique public link at `/connect/{your-slug}`. Open it yourself to connect your own number, or share it — by email, chat, or a button on your own site — with a business whose numbers you manage. It shows your brand name and a **Connect WhatsApp** button. ## What happens during signup 1. ### Meta Embedded Signup opens The person connecting authenticates with Facebook and selects (or creates) the WhatsApp Business Account and phone number to use. 2. ### Authorization is exchanged On completion, the platform exchanges a one-time code for a long-lived access token, which is stored **encrypted**. The token never leaves the server. 3. ### The number appears in your dashboard A `Company` (named after the business Meta returns) and its `Connection` — the WABA plus the phone number — are created under your account. The WABA is subscribed to webhooks so statuses flow in, and the number is registered on the Cloud API so it can send right away. > [!NOTE] > **Numbers already on the WhatsApp Business app** > > A coexistence number stays on the WhatsApp Business app and is registered by Meta itself, so we > leave its registration alone — it connects and sends exactly the same way. If a registration ever > fails (an existing two-step PIN, for instance) the connection is still stored, and the number > becomes usable once it is registered on Meta's side. > [!NOTE] > **What you can see afterwards** > > For every connected business: the WABA, its phone number(s) with quality rating and status, every > template with its approval state, and message delivery stats (sent / delivered / read / failed > with reasons). ## Knowing WHICH client connected WHICH number If you share your link with several clients, put your own identifier for that client in it: ``` https://wa.genuka.com/connect/{your-slug}?ref=cli_8f3a91e4 ``` The reference lands on the `Company` created at the end of the flow, and you read it back: ```bash title="GET /api/v1/connections?externalRef=cli_8f3a91e4" curl "https://wa.genuka.com/api/v1/connections?externalRef=cli_8f3a91e4" \ -H "Authorization: Bearer pk_live_xxx" { "data": [ { "id": "con_1", "companyId": "cmp_123", "companyName": "Acme Coffee", "displayPhoneNumber": "+237 6 90 …", "externalRef": "cli_8f3a91e4", "qualityRating": "GREEN", "status": "connected" } ] } ``` One row: that client's. It spares you both wrong answers to this question — taking the most recent connection, which eventually hands one client's number to another the day two of them connect in the same minute; or showing the list and asking your client to point at their own, which discloses every other client's number and business name on the way. > [!NOTE] > **Pick an unguessable reference** > > It travels in a URL. A sequential identifier (`client-42`) can be guessed, and whoever guesses > it can attach their own number to that client. Use a UUID or equivalent randomness. Accepted characters: letters, digits, `.` `_` `:` `-`, 128 max. A missing or malformed reference is never quietly widened to the full list — the lookup simply returns zero rows. Sending the same reference again on a reconnection moves it to the number that just connected: it is your ledger, not ours. ## Multiple numbers, limits & re-connection You can connect as many numbers as your subscription pays for, spread across as many businesses as you like. Connecting a brand-new number takes a free slot; going past the number count you paid for is refused with `402 plan_limit_numbers`, and raising it takes a new payment from the Billing page. Re-running the connect flow for a number that is already connected refreshes its token and details rather than creating a duplicate — and never consumes a slot. --- # Authentication URL: https://wa.genuka.com/en/docs/authentication Language: English > API keys, scope, and rotation practices. Every request authenticates with an API key in the `Authorization` header: ```http Authorization: Bearer pk_live_... ``` > [!WARNING] > You **never** handle a Meta token. Genuka WA holds the Cloud API credentials and rotates them > for you: one Genuka API key is enough, and it only reaches your own data. ## Two key scopes | Scope | Access | Use case | | ----------- | --------------------------- | -------------------------------------------------- | | **Partner** | Every client of the partner | A platform managing numbers for several businesses | | **Client** | A single business | A business integrating only its own numbers | A client key reaching for another business gets `403 out_of_scope`. Some endpoints — billing, plan usage — require a partner key and answer `403 partner_key_required` otherwise. ## Creating and revoking a key Keys are managed from the portal, under **Developers**. A key is shown **once** at creation: Genuka stores only a fingerprint and cannot show it to you again. To rotate without downtime: create the new key, deploy it, confirm traffic in the logs, then revoke the old one. ## Error responses | Status | Code | Meaning | | ------ | ---------------------- | ----------------------------------------- | | `401` | `missing_bearer_token` | No `Authorization` header | | `401` | `invalid_token` | Unknown or revoked key | | `403` | `out_of_scope` | The key is restricted to another business | | `403` | `partner_key_required` | Endpoint reserved for partner keys | ## Security * An API key is a server-side secret. Never ship it in a frontend or a mobile app — anyone could send messages in your name, at your expense. * Use distinct keys per environment and per integration: revoking then becomes surgical rather than global. * Every request is logged and visible in the portal. --- # Sending messages URL: https://wa.genuka.com/en/docs/messages Language: English > One endpoint covering the whole Cloud API surface. ```http POST /api/v1/messages ``` A send always carries three things: the connection to send from, the recipient, and **exactly one content field**. ```json { "connectionId": "cnx_...", "to": "+237699001122", "text": "Hello 👋" } ``` The number is in international format. Get `connectionId` from `GET /api/v1/connections`. Add `replyTo` with the `wamid` of an inbound message to quote it in your reply. ## Content fields > [!WARNING] > **One field at a time** > > Sending two content fields in the same request does not send two messages: only the first > recognised one is used. Make two calls. ### Text and media | Field | Content | Limits | | ---------- | ----------------------------------- | ----------------------------- | | `text` | A string, or `{ body, previewUrl }` | 4096 characters | | `image` | JPEG, PNG | 5 MB, caption 1024 characters | | `video` | MP4, 3GPP (H.264 + AAC) | 16 MB | | `audio` | AAC, AMR, MP3, M4A, OGG | 16 MB, **no caption** | | `document` | PDF, DOC(X), XLS(X), PPT(X), TXT | 100 MB | | `sticker` | Static or animated WebP | 100 KB / 500 KB | ### Location, contacts, reaction | Field | Content | | ----------------- | ------------------------------------------------------------- | | `location` | `{ latitude, longitude, name, address }` | | `locationRequest` | `{ body }` — shows a "Send location" button | | `contacts` | An array of contact cards | | `reaction` | `{ messageId, emoji }` — an empty string removes the reaction | ### Interactive messages ```json { "connectionId": "cnx_...", "to": "+237699001122", "buttons": { "body": "How can we help?", "footer": "Genuka", "buttons": [ { "id": "order", "title": "My order" }, { "id": "support", "title": "A problem" } ] } } ``` | Field | Description | Limits | | --------- | ------------------------------ | --------------------------------- | | `buttons` | Quick-reply buttons | **3 max**, 20-character titles | | `list` | Sections and rows | 10 sections, **10 rows in total** | | `cta` | A single button carrying a URL | `url` + `displayText` | The user's answer arrives by webhook, carrying the `id` of the button or row they picked. ## Templates Outside the 24-hour window, only an approved template will be delivered. ```json { "connectionId": "cnx_...", "to": "+237699001122", "template": { "name": "order_confirmation", "language": "en", "body": ["Awa", "ORD-1042"] } } ``` > [!NOTE] > A template send opens a billable Meta conversation on the client's own WABA — Meta bills the > client directly. Service messages sent inside the 24-hour window are free. Create and track templates through `/api/v1/templates`. A template in `PAUSED` or `REJECTED` status cannot be used: check its status before launching a campaign. ## Escape hatch If a message type is not exposed yet, `raw` forwards a Cloud API fragment verbatim: ```json { "connectionId": "cnx_...", "to": "+237699001122", "raw": { "type": "interactive", "interactive": { "type": "flow" } } } ``` No validation is applied to `raw` — errors come back from Meta as-is. ## Common errors | Meta code | Meaning | What to do | | --------- | ------------------------------------------------- | ------------------------------- | | `131047` | More than 24h since the last inbound message | Send a template | | `131026` | Recipient unreachable or not on WhatsApp | Do not retry | | `131050` | User opted out of marketing | Never send them marketing again | | `131049` | Per-user marketing cap reached | Retry later | | `132001` | Template missing or not approved in that language | Check name and language | | `130429` | Throughput exceeded | Slow down, retry with backoff | --- # Templates & campaigns URL: https://wa.genuka.com/en/docs/campaigns Language: English > Templates are pre-approved messages; campaigns send them to a list of recipients. ## Message templates To message someone outside the 24-hour service window, you use an approved template (e.g. marketing or utility). Templates are reviewed by Meta and move through these states: * `draft` — created locally, not yet submitted. * `submitted` / `pending` — sent to Meta, awaiting review. * `approved` — ready to use in campaigns. * `rejected` — refused by Meta, with a reason shown in the dashboard. * `disabled` — paused or flagged by Meta. Each template has a **category** (`MARKETING`, `UTILITY`, `AUTHENTICATION`), a **language**, and a set of components (header, body, footer, buttons). > [!NOTE] > **Status updates are automatic** > > When Meta approves or rejects a template, the change is pushed to the platform and reflected in > your dashboard within seconds — no manual refresh needed. ## Campaigns A campaign sends a chosen template to a list of recipients from one connection. Each recipient is tracked individually with a delivery status: * `pending` — queued, not yet sent. * `sent` → `delivered` → `read` — normal progression. * `failed` — with the failure reason returned by Meta. Genuka is a Meta Tech Provider, not a BSP — Meta bills your own WABA directly, so we never debit credits per message. Instead, every recipient counts against your plan's monthly message allowance (the numbers you pay for × the tier's per-number quota), exactly like a single `POST /api/v1/messages` send does. The whole recipient list is checked before the first send, so a launch that wouldn't fit returns `402 plan_limit_messages` up front rather than stopping half-way; add numbers or move up a tier to raise the allowance. See [Plans & billing](https://wa.genuka.com/en/docs/billing). > [!WARNING] > **Quality matters** > > Sending to recipients who haven't opted in, or high block/report rates, can lower a number's > quality rating and lead Meta to restrict it. Monitor quality on the number's page in the > dashboard. --- # Webhooks URL: https://wa.genuka.com/en/docs/webhooks Language: English > Receive inbound messages, delivery receipts and status changes. You register a URL, Genuka WA delivers every event there as a signed `POST`. ## Registering an endpoint In the portal, under **Webhooks**: enter the URL (HTTPS required) and tick the events you want. The **signing secret** (`whsec_…`) is shown once, at creation. An endpoint can cover every number on your account (general), one connected business, or a single phone number — and a business can be opted out of a general endpoint. ## Available events | Event | Contents | | -------------------------------- | ---------------------------------------------------------- | | `messages` | Inbound messages **and** statuses of the messages you sent | | `message_template_status_update` | A template was approved, rejected or paused | | `account_update` | Account change: verification, restriction, suspension | | `phone_number_quality_update` | A number's quality or sending tier changed | The platform keeps no inbox: webhooks are how you read replies. ## The payload Every delivery is a `POST` carrying one event. `data` is the raw Meta value, untouched; the fields around it tell you which business and number it belongs to. ```json title="POST https://your-app.com/webhooks/whatsapp" { "id": "dlv_2f8c1a...", "type": "message_status", "field": "messages", "created_at": "2026-08-04T09:31:07.412Z", "partner_id": "cl9x...", "company_id": "cm31...", "connection_id": "cn77...", "waba_id": "102290129340398", "phone_number_id": "106540352242922", "data": { "id": "wamid.HBgLMjM3...", "status": "delivered", "timestamp": "1785836801", "recipient_id": "237699000000" } } ``` ### Headers | Header | Contents | | ---------------------- | -------------------------------------------------------------- | | `X-Genuka-Signature` | The signature — see below | | `X-Genuka-Event` | Event type, e.g. `message_status` | | `X-Genuka-Event-Field` | The underlying Meta webhook field | | `X-Genuka-Delivery` | Delivery id, identical across retries — use it to de-duplicate | | `X-Genuka-Webhook-Id` | The endpoint that was targeted | | `X-Genuka-Attempt` | Attempt number, starting at 1 | ## Verifying the signature The header is `X-Genuka-Signature`, shaped `t=1785836801,v1=5f3c…`, where `v1` is the hex HMAC-SHA256 of `` `${t}.${rawBody}` `` keyed with your secret. The timestamp is **bound into the MAC**: that is what stops a captured request from being replayed. A receiver rejects any `t` outside its tolerance window (300 seconds by default), and `t` cannot be edited without invalidating `v1`. > [!WARNING] > Sign the **raw body**, exactly as received. A `JSON.parse` followed by a `JSON.stringify` changes > key order and whitespace: the signature will never match. ```ts import { verifySignature } from "@genuka/whatsapp/webhooks"; export async function POST(request: Request) { const raw = await request.text(); const signature = request.headers.get("x-genuka-signature"); if (!(await verifySignature(process.env.WEBHOOK_SECRET!, raw, signature))) { return new Response("invalid signature", { status: 401 }); } // Process asynchronously, answer straight away. void handle(JSON.parse(raw)); return new Response("ok"); } ``` > [!NOTE] > **Minimum version: @genuka/whatsapp 0.1.1** > > `verifySignature` recognises both schemes and applies the matching algorithm: Genuka WA's > timestamped format (`t=…,v1=…`) and Meta's (`X-Hub-Signature-256`), the latter only useful if you > also receive Meta webhooks directly. In 0.1.0 only Meta's format was handled — verifying a > Genuka WA webhook failed every time. If you are not using the library, the algorithm is a few lines: ```ts title="verify-signature.ts" import crypto from "node:crypto"; const TOLERANCE_SECONDS = 300; export function verify(rawBody: string, header: string, secret: string): boolean { const parts = Object.fromEntries(header.split(",").map((p) => p.split("="))); const timestamp = Number(parts.t); // Reject anything outside the window: this is what makes a captured // request useless to replay later. if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false; const expected = crypto .createHmac("sha256", secret) .update(`${timestamp}.${rawBody}`) .digest("hex"); return crypto.timingSafeEqual( Buffer.from(parts.v1, "utf8"), Buffer.from(expected, "utf8"), ); } ``` > [!NOTE] > **Rotate the secret when you suspect exposure** > > Rotation takes effect immediately: the previous secret stops verifying as soon as you rotate, so > update your receiver in the same window. ## Three processing rules > [!WARNING] > **Answer 200 immediately** > > Do your processing in the background. A slow endpoint triggers re-deliveries, which amplify load > at exactly the wrong moment. **Events can be duplicated.** The same message may be delivered to you twice. Make your processing idempotent using the message id (`wamid`) or `X-Genuka-Delivery`. **Order is not guaranteed.** A read receipt can arrive before the delivery receipt for the same message. Never regress a status: the logical order is `sent` → `delivered` → `read`, and `failed` is terminal. ## Statuses of a sent message | Status | Meaning | | ----------- | --------------------------------------------- | | `sent` | Accepted by WhatsApp's servers | | `delivered` | Reached the recipient's device | | `read` | Read — only if the user enabled read receipts | | `failed` | Failed, with Meta's error code | | `deleted` | Message deleted | ## Retries & failures A delivery counts as successful on any `2xx`. Anything else — including a timeout past 10 seconds — is retried 5 times with growing backoff (1 min, 5 min, 30 min, 2 h, 6 h), then marked failed. ## Log and replay The portal keeps the delivery history: payload, HTTP status, attempt count and your server's response. If something broke on your side, you can replay an event once your endpoint is back. > [!NOTE] > An API key plays no part in receiving webhooks: the only thing to check on the receiving end is > the signature. Never trust the contents of an unsigned event. --- # Plans & billing URL: https://wa.genuka.com/en/docs/billing Language: English > You pay per WhatsApp number. Meta bills the conversations on your Business Account directly — we never mark up a message. ## The free trial Every new account opens on a **7-day free trial** of the Growth tier, with one WhatsApp number and its full message allowance. No card is required to start. Pick a plan before the trial ends to keep sending: once it lapses the account goes `past_due` — the API stops accepting sends, but the dashboard stays readable and nothing is deleted. Paying reactivates everything as it was. ## Who bills what Genuka operates as a Meta Tech Provider, not a BSP (Business Solution Provider). You connect your own WhatsApp Business Account (WABA), and **Meta bills that WABA directly** for conversations and messages. Genuka never sits in the message-cost path: we do not charge for a message, and we never mark one up. There are no per-message credits and no wallet. > [!NOTE] > **Quota, not metering** > > Your plan includes a monthly allowance of outbound messages per number, and we count sends against > it. That is a subscription quota — you are never invoiced per message, by us or through us. ## The billing unit is the number A plan is a **base price that covers your first number, plus a cheaper rate for every extra one**. That extra rate falls as the tier rises — 2,500, 2,200 then 1,800 XAF per month — so growing is what buys the discount. At checkout you pick a tier and a total number count; that count is fixed for the billing period (mobile money is a one-shot collection with no proration) and it does two things: * **It caps your connections.** Connecting one more than you paid for takes a new payment. * **It sizes your message allowance.** The period's quota is your number count times the tier's per-number allowance. > [!NOTE] > **Example** > > 3 numbers on Starter, billed monthly, costs 5,000 + 2 × 2,500 = 10,000 XAF and includes > 3 × 500 = 1,500 outbound messages for the period. ## The tiers Four tiers. Every one of them includes REST API access and inbound webhooks — receiving replies is the primary job, so it is never gated: * **Starter** — up to 5 numbers (+2,500 XAF each), 500 messages per number per month, 1 user, standard support. * **Growth** — up to 15 numbers (+2,200 XAF each), 5,000 messages per number per month, 5 users, white-label branding and priority support. * **Scale** — up to 100 numbers (+1,800 XAF each), 30,000 messages per number per month, 15 users, client sub-accounts and dedicated support. * **Enterprise** — quote-only: custom volume, custom terms, contractual SLA. ### Intervals & currencies Plans are billed **monthly or annually**, and paying yearly is cheaper per number — the discount depends on the tier, up to 40% on Starter. Pricing is available in **XAF, XOF, EUR, and USD**. XAF and XOF are paid by mobile money, EUR and USD by card. ## Plan limits Three numeric limits, all readable from the API. Feature gates (white-label, client sub-accounts, priority support) are toggled per tier on top of them: * **Numbers** — how many WhatsApp numbers you may connect, i.e. how many you paid for. * **Messages per period** — outbound messages included, counted across API sends and campaign recipients alike. Resets at the start of each billing period. * **Users** — dashboard accounts on the workspace. ## When you hit a limit Exceeding a limit does not silently fail. The API rejects the request with HTTP `402` and an error code identifying which limit was reached: * `plan_limit_numbers` — you already have every number your plan paid for. * `plan_limit_messages` — the period's message allowance is exhausted. * `subscription_past_due` — the trial or the paid period lapsed without a renewal. Add numbers to your plan, move up a tier, or wait for the next billing period. A campaign is checked against the whole recipient list before the first send, so a launch that would blow the quota is refused up front rather than stopping half-way through. ## Payments Paid plans in XAF and XOF are collected by mobile money: through **Genuka Pay** in Cameroon and **pawaPay** in other countries (MTN, Orange, Wave, Moov…). Plans in EUR and USD are paid by card through **Stripe** (hosted checkout). No payment method carries a recurring mandate, so each period is a fresh collection you trigger yourself — we never charge anything without your approval. ## Your invoices Every billed period gets an invoice, numbered in sequence (`INV-2026-0042`). There are two kinds, and they read and settle the same way: * **On checkout** — you pick a tier, the invoice is issued and settled in one go. * **On renewal** — the next period's invoice is issued **7 days before** the current one ends, due on the day it does. You always know how much and by when, before anything is interrupted. They all live in the dashboard under **Billing > See all invoices**: paid, to pay, overdue or void. Each one opens in full — period covered, base and additional numbers line by line, payment attempts — and prints or saves to PDF straight from your browser. An unpaid one settles in one click, by mobile money. > [!NOTE] > **Paying early costs you nothing** > > Settling the renewal invoice ahead of its due date does not shorten the period you are on: the > new one starts the day the current one ends. No paid day is lost. An invoice can be **voided** in two cases, and nothing is owed on it then: you changed tier before settling it (a newer one replaces it), or you cancelled at the end of the period. ## Managing your plan Review and manage your plan in the dashboard under **Billing**, or read your subscription and your invoices programmatically: | Method | Endpoint | Description | | ------ | ----------------------- | -------------------------------------------------------------------- | | `GET` | `/api/v1/subscription` | Your current plan, limits, usage, and billing period. | | `GET` | `/api/v1/invoices` | Your invoices, newest first. Filter with `?status=open\|paid\|void`. | | `GET` | `/api/v1/invoices/{id}` | One invoice, with the payment attempts made against it. | Each invoice carries a stored `status` (`open`, `paid`, `void`) and a derived `state`, which splits `due` from `overdue` — that one is a clock reading, not a column. --- # WhatsApp Business API guides URL: https://wa.genuka.com/en/docs/guides Language: English > WhatsApp Business API guides with Genuka WA: OTP codes, order notifications, campaigns, webhooks, coexistence, Meta pricing and AI agents. Each of these guides answers one integration question about the WhatsApp Business API with Genuka WA: sending an OTP code, notifying customers about an order, running a campaign, receiving replies and statuses on a webhook, keeping the WhatsApp Business app on your number, starting without Meta verification, estimating message costs in Africa, or handing WhatsApp to an AI agent. *Last updated October 8, 2026* ## How do I send WhatsApp messages from my backend? * [Send WhatsApp OTP codes from Node.js](https://wa.genuka.com/en/docs/guides/whatsapp-otp-nodejs): Authentication template with a copy-code button, server-side expiry and verification, SMS fallback. * [WhatsApp order notifications](https://wa.genuka.com/en/docs/guides/order-notifications): Confirmation, shipping and delivery as utility templates: variables, idempotency and delivery statuses. * [Send a WhatsApp campaign through the API](https://wa.genuka.com/en/docs/guides/campaigns-api): Marketing template, recipient list, launch, plan quotas and Meta's marketing limits. ## How do I receive replies and delivery statuses? * [Receive replies and statuses via webhook](https://wa.genuka.com/en/docs/guides/receive-messages-webhooks): Register a URL, verify the HMAC signature in the `X-Genuka-Signature` header, deduplicate, handle retries and replay. ## How do I connect my WhatsApp number? The procedure itself is in [Connecting a number](https://wa.genuka.com/en/docs/onboarding). These two guides answer the questions that come before it. * [Coexistence with the WhatsApp Business app](https://wa.genuka.com/en/docs/guides/coexistence): Keep the app on the number while sending through the API: requirements, steps, what syncs and the limits. * [WhatsApp API without Meta verification](https://wa.genuka.com/en/docs/guides/without-meta-verification): What an unverified business can send, what verification unlocks, and the risk of unofficial APIs. ## How much does a WhatsApp message cost? * [WhatsApp message prices in Africa](https://wa.genuka.com/en/docs/guides/whatsapp-pricing-africa): Meta's rates by country and category, in USD, EUR and CFA francs, with a worked example. Those are Meta's rates. The Genuka WA subscription, billed per number, is on the [pricing page](https://wa.genuka.com/en#pricing) and in Markdown at [/pricing.md](https://wa.genuka.com/pricing.md). ## How do I plug an AI agent into WhatsApp? * [WhatsApp API for AI agents](https://wa.genuka.com/en/docs/guides/ai-agents): The `@genuka/whatsapp-mcp` MCP server for Claude, Cursor or VS Code, plus `llms.txt`, Markdown pages and the OpenAPI spec for writing the integration. ## What do I need before following a guide? A Genuka WA account, a connected number and an API key. The [quickstart](https://wa.genuka.com/en/docs/quickstart) covers all three, [authentication](https://wa.genuka.com/en/docs/authentication) covers the key, and the [API reference](https://wa.genuka.com/en/docs/api) lists every endpoint. The same API is described in OpenAPI 3.1 at [/openapi.json](https://wa.genuka.com/openapi.json). When a call fails, the code Meta returned is explained under [error codes](https://wa.genuka.com/en/docs/errors). ## FAQ ### Do I need Meta business verification to follow these guides? Not to get started. An unverified business sends marketing and utility templates to up to 250 unique recipients per 24 hours outside the customer service window to start with ([Meta, Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits)). OTP codes are the exception: authentication templates require clearing one of Meta's scaling paths, business verification in most cases ([360dialog, Authentication messages](https://docs.360dialog.com/docs/resources/authentication-messages)). The details are in [WhatsApp API without Meta verification](https://wa.genuka.com/en/docs/guides/without-meta-verification). ### Can I keep a number that already runs on WhatsApp Business? Yes. With Meta's coexistence, the WhatsApp Business app, version 2.24.17 or later, keeps working on the number connected to the API, and one-to-one chat history stays in sync ([Meta, Onboarding WhatsApp Business app users](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users)). Only a number registered on regular WhatsApp, the consumer app, has to be freed first. See the [coexistence guide](https://wa.genuka.com/en/docs/guides/coexistence). ### Who bills the messages I send? Meta, directly on your WhatsApp Business account: a client onboarded by a Tech Provider adds its own payment method, and Meta bills it for usage ([Meta, Solution Partners and Tech Providers](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview)). Genuka takes no markup on those rates and charges a subscription per WhatsApp number. ### Which languages are the code samples in? curl, Node.js, Python and PHP in most guides. The webhooks guide shows Next.js, Express, Flask and the `@genuka/whatsapp` library; the OTP guide sticks to Node.js and curl. ## Sources Read on October 8, 2026. * Meta — [Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits) * Meta — [Onboarding WhatsApp Business app users (coexistence)](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users) * Meta — [Solution Partners and Tech Providers](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview) * 360dialog — [Authentication messages](https://docs.360dialog.com/docs/resources/authentication-messages) --- # Send WhatsApp OTP codes from Node.js URL: https://wa.genuka.com/en/docs/guides/whatsapp-otp-nodejs Language: English > Send WhatsApp one-time passwords from Node.js with Genuka WA: authentication template, copy-code button, expiry, verification and SMS fallback. To send a WhatsApp one-time code from Node.js with Genuka WA, create an `AUTHENTICATION` template with a copy-code button once, then call `POST /api/v1/messages` with the `otp` shorthand. Meta writes the message text; you only supply the code, and your server owns expiry and verification. The step people miss: the business must first pass Meta business verification (or another Meta scaling path). *Last updated October 8, 2026* ## What do I need before I start? * A number connected to Genuka WA and its `connectionId` — see the [quickstart](https://wa.genuka.com/en/docs/quickstart). * An API key, used **server-side only** — see [Authentication](https://wa.genuka.com/en/docs/authentication). * Node.js 22 or later: a still-supported LTS line, with native `fetch`. * A business that has passed **Meta business verification** (or another Meta scaling path). This is the requirement people find too late: without it Meta refuses to create the template, with an error message that blames the app rather than the business. The last section of this guide covers it. * **A payment method on the WhatsApp Business account, at Meta.** Genuka is a Meta Tech Provider, not a BSP: Meta bills the account directly, and a client onboarded by a Tech Provider must add its own payment method ([Meta, Partners](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview)). Without one, the template gets approved but every code comes back with error `131042` — see [error 131042](https://wa.genuka.com/en/docs/errors/131042). ## How does a WhatsApp OTP work? A one-time code travels in an `AUTHENTICATION`-category template. Unlike the other categories, you do not write its text: Meta supplies a preset body and inserts your code into it, and you only pick the options around it ([Meta docs](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/authentication-templates/authentication-templates)). | Part | What you choose | Meta's rule | | ------------------ | -------------------------------------------- | ---------------------------------------------------------------------- | | Body | Nothing — preset text, your code is inserted | No custom text | | Security notice | `add_security_recommendation: true` | Optional | | Footer | `code_expiration_minutes` | 1 to 90 minutes | | Button | `OTP` with `otp_type: COPY_CODE` | Label up to 25 characters | | Code | Generated by your server | Up to 15 characters | | Time-to-live (TTL) | `messageSendTtlSeconds` | Meta: 10 min by default, 30 s to 15 min; Genuka WA accepts 60 to 600 s | URLs, media and emojis are not supported in this template type ([Meta docs, copy code button](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/authentication-templates/copy-code-button-authentication-templates)). > [!NOTE] > **Copy code, one-tap or zero-tap?** > > `COPY_CODE` works on every phone: the user taps the button and pastes the code. `ONE_TAP` and > `ZERO_TAP` hand the code to your Android app and need its `package_name` and `signature_hash` — > see the [API reference](https://wa.genuka.com/en/docs/api#templates). Start with `COPY_CODE`. ## How do I create the authentication template? 1. ### Submit the template One call, once. Note the language you choose: every send must use exactly the same one. **curl** ```bash title="POST /api/v1/templates" curl -X POST https://wa.genuka.com/api/v1/templates \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "name": "login_code", "language": "en", "category": "AUTHENTICATION", "messageSendTtlSeconds": 300, "components": [ { "type": "BODY", "add_security_recommendation": true }, { "type": "FOOTER", "code_expiration_minutes": 5 }, { "type": "BUTTONS", "buttons": [ { "type": "OTP", "otp_type": "COPY_CODE", "text": "Copy code" } ] } ] }' ``` **Node.js** ```ts title="create-otp-template.ts" const response = await fetch("https://wa.genuka.com/api/v1/templates", { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ connectionId: process.env.GENUKA_WA_CONNECTION_ID, name: "login_code", language: "en", category: "AUTHENTICATION", // Past 5 minutes Meta stops trying to deliver: the code would have expired anyway. messageSendTtlSeconds: 300, components: [ { type: "BODY", add_security_recommendation: true }, { type: "FOOTER", code_expiration_minutes: 5 }, { type: "BUTTONS", buttons: [{ type: "OTP", otp_type: "COPY_CODE", text: "Copy code" }], }, ], }), }); console.log(response.status, await response.json()); ``` The `201` response holds the stored template, with its `id` and `status`. In the dashboard, the same thing is **Templates** › **New template**, Authentication category, "Copy code" button. 2. ### Wait for approval Meta reviews the template. The verdict reaches you in three ways: * a `template_status` webhook whose `data.event` is `APPROVED` or `REJECTED`, if your endpoint subscribes to `template.status_changed` (or to every event); * `GET /api/v1/templates/{id}`, which returns the status and its history; * `POST /api/v1/templates/sync`, which re-reads statuses from Meta if a webhook went missing. Only an `approved` template can be sent. ## How do I send the code from Node.js? The `otp` shorthand puts the code in both places Meta requires for an authentication template: the body parameter and the button parameter. You do not write the `components` yourself. **Node.js** ```ts title="otp.ts" import crypto from "node:crypto"; const API = "https://wa.genuka.com/api/v1"; const CODE_TTL_MS = 5 * 60_000; // matches code_expiration_minutes and messageSendTtlSeconds const MAX_ATTEMPTS = 5; type OtpEntry = { codeHash: string; expiresAt: number; attempts: number; messageId: string }; /** The response body: `data` on success, otherwise `error` (plus `meta` if Meta refused). */ type ApiResult = { data?: { messageId: string }; error?: string; message?: string; meta?: { errorClass?: string; retryable?: boolean; code?: number }; }; // In memory for the example. In production: Redis or your database, with the same expiry, // or a second server will not know about the code the first one issued. const store = new Map(); const hash = (phone: string, code: string) => crypto.createHash("sha256").update(`${phone}:${code}`).digest("hex"); export async function sendOtp(phone: string): Promise { // A cryptographic generator, never Math.random(). const code = crypto.randomInt(0, 1_000_000).toString().padStart(6, "0"); const response = await fetch(`${API}/messages`, { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ connectionId: process.env.GENUKA_WA_CONNECTION_ID, to: phone, // international format with the +, e.g. "+237699001122" // Always set `language`: without it the send targets "en_US". template: { name: "login_code", language: "en", otp: code }, }), }); // A 5xx from the CDN is not JSON: without this catch, the parse error would hide the status. const body = (await response.json().catch(() => ({}))) as ApiResult; if (!response.ok || !body.data) { // body.meta.errorClass tells you who can fix it: see "What if the code never arrives?" throw new Error(`OTP not sent: ${response.status} ${body.error ?? ""} — ${body.message ?? ""}`); } // A new code replaces the previous one: one valid code per number at a time. store.set(phone, { codeHash: hash(phone, code), expiresAt: Date.now() + CODE_TTL_MS, attempts: 0, messageId: body.data.messageId, }); return body.data.messageId; } ``` **curl** ```bash title="POST /api/v1/messages" curl -X POST https://wa.genuka.com/api/v1/messages \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "to": "+237699001122", "template": { "name": "login_code", "language": "en", "otp": "482913" } }' ``` ```json title="Response" { "data": { "messageId": "wamid.HBg…" } } ``` A `200` means Meta **accepted** the message, not that it arrived. Delivery is confirmed by webhook, carrying the same `messageId`. > [!WARNING] > **The language must match exactly** > > To Meta, `en` and `en_US` are two distinct language codes > ([supported languages](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/supported-languages)): > a template approved in `en` does not exist in `en_US`. Sending in a language the template does > not exist in, or is not approved in, fails with error `132001` > ([Meta error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes)). ## How do I verify the code the user typed? Verification never goes through WhatsApp: it happens entirely on your side. ```ts title="otp.ts (continued)" export function verifyOtp(phone: string, input: string): boolean { const entry = store.get(phone); if (!entry || Date.now() > entry.expiresAt) return false; // Cap attempts: a 6-digit code falls to 1,000,000 guesses at worst, far fewer if nobody counts. if (entry.attempts >= MAX_ATTEMPTS) return false; entry.attempts += 1; // Two hex SHA-256 digests: same length, constant-time comparison. const ok = crypto.timingSafeEqual( Buffer.from(entry.codeHash, "hex"), Buffer.from(hash(phone, input.trim()), "hex"), ); if (ok) store.delete(phone); // single use return ok; } ``` Four rules hold it together: one code per number at a time, single use, a bounded number of attempts, and an expiry decided by your server. ## How should I set expiry and time-to-live? Three durations coexist. Set them to the same value. | Duration | Where it is set | What it does | | ------------------------- | --------------- | ------------------------------------------- | | `code_expiration_minutes` | Template footer | What the user reads: "expires in 5 minutes" | | `messageSendTtlSeconds` | Template | Past it, Meta stops trying to deliver | | Server expiry | Your database | The only one that counts when you verify | When a message cannot be delivered, WhatsApp retries for the template's time-to-live and then drops it; if no `delivered` or `read` status arrived by then, treat the message as lost ([Meta docs, time-to-live](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/time-to-live)). A code delivered after it expired is worse than one never delivered: the user types it, it is refused, and they cannot tell why. ## What if the code never arrives? Track the message by its `messageId` in your webhook receiver — see [Receive replies and statuses via webhook](https://wa.genuka.com/en/docs/guides/receive-messages-webhooks) for signature verification. ```ts title="In your webhook receiver (signature already verified)" if (event.type === "message_status") { const { id: messageId, status, errors } = event.data; // `read` counts as delivered: Meta sometimes sends no `delivered` (see below). if (status === "delivered" || status === "read") await markOtpDelivered(messageId); if (status === "failed") await markOtpUndeliverable(messageId, errors?.[0]?.code); } ``` Do not wait for `delivered` alone. When the user has the chat open as the message lands — the usual case for a code they are waiting for — Meta treats it as delivered and read at once and sends only `read` ([Meta docs, statuses](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status)). Then, in your UI: * **Explicit failure**: offer another channel right away. Meta itself recommends letting users choose between WhatsApp, email and SMS ([Meta best practices](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/authentication-templates/authentication-best-practices)). * **No `delivered` or `read` after about thirty seconds**: show "Get the code by SMS" rather than sending a second WhatsApp code to the same number. * **Resend**: enforce a delay between requests, and invalidate the previous code on every send. | Meta code | Meaning | Reaction | | --------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | `131026` | Message undeliverable, for example a number without WhatsApp | Switch to SMS, do not retry | | `131056` | Too many messages to the same recipient in a short period | Wait before resending | | `130429` | Cloud API throughput reached | Retry with growing backoff | | `132001` | Template missing in that language or not approved | Fix the name or language | | `131042` | Payment method problem on the client's Meta account: no code leaves | Switch to SMS; the client fixes billing at Meta — see [error 131042](https://wa.genuka.com/en/docs/errors/131042) | When Meta refuses the send on the spot, or a template or number check blocks it before the call, the response is a `400` (`409` for a configuration problem) whose `error` is `send_`, with a `meta` object carrying `errorClass` and Meta's `code`. The class settles it: `recipient_permanent` (switch channel), `retryable` or `recipient_throttled` (try again later), `template` or `config` (the problem is yours, not the user's — alert the team). Genuka WA's own refusals carry no `meta`: read `error`. `400 missing_fields` and `400 recipient_is_sender` are bugs in the request. The others block every code until the account is fixed: `402 subscription_past_due` (the trial or the paid period has lapsed), `402 plan_limit_messages` (the period's allowance is used up), `409 number_released`, `403 account_deactivated` and `404 connection_not_found`. On any of them, send the code by SMS or email straight away and alert the team: the user is waiting at the sign-in screen. ## Why does Meta refuse to create my authentication template? Meta reserves the `AUTHENTICATION` category for accounts that meet two conditions: they have completed one of its scaling paths — in practice Meta business verification or partner-led verification — and they have a messaging limit of at least 2,000 ([360dialog, authentication messages documentation](https://docs.360dialog.com/docs/resources/authentication-messages)). The `MARKETING` and `UTILITY` categories are not affected. Here is what we see on accounts connected to Genuka WA when the business is not verified: * creation answers `meta_rejected` with Graph's message "Application does not have permission for this action". It names the app, but the cause is the client's business: no key or permission setting will change it; * the WhatsApp Business account's `health_status` carries error **141010**, "The Business has not passed business verification". The fix is to complete business verification in the client's Meta Business Suite, then create the template again. Details are on the [error 141010](https://wa.genuka.com/en/docs/errors/141010) page. You can also read a hint in `GET /api/v1/numbers/{connectionId}/health`: the `messagingLimitTier` field. A new business portfolio starts at 250 unique recipients per 24 hours outside the customer service window; business verification is one of the ways that takes it to 2,000 ([Meta docs, messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits)). ## FAQ ### Can I customise the text of the OTP message? No. The body of an authentication template is fixed by Meta; you only set the security notice, the duration shown in the footer and the button label. ### How much does a WhatsApp OTP cost? Meta charges for delivered templates by category and recipient country ([Meta pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing)), billed straight to the client's WhatsApp Business account. Genuka WA adds no markup: each send simply counts against your subscription's message allowance — see the [plans](https://wa.genuka.com/en#pricing). ### Does the user have to message me before receiving a code? No: a template can be sent outside the 24-hour window. You do need the person's agreement, and Meta sets two conditions: state clearly that they are opting in to receive messages from your business, and name that business ([Meta docs, opt-in](https://developers.facebook.com/documentation/business-messaging/whatsapp/getting-opt-in)). On the sign-in screen, a line such as "Get my sign-in code from \[Business name] on WhatsApp" next to the phone number field covers both. ### What happens if the number has no WhatsApp account? The status comes back `failed` with code `131026`. Do not retry on WhatsApp: offer SMS or email. ## Sources * [Meta — Authentication templates](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/authentication-templates/authentication-templates) * [Meta — Copy code authentication templates](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/authentication-templates/copy-code-button-authentication-templates) * [Meta — Time-to-live](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/time-to-live) * [Meta — Authentication best practices](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/authentication-templates/authentication-best-practices) * [Meta — Supported languages](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/supported-languages) * [Meta — Status messages webhook](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status) * [Meta — Getting opt-in](https://developers.facebook.com/documentation/business-messaging/whatsapp/getting-opt-in) * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Partners (Tech Providers and Solution Partners)](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview) * [Meta — Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits) * [Meta — Pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing) * [360dialog — Authentication messages](https://docs.360dialog.com/docs/resources/authentication-messages) --- # WhatsApp order notifications from your backend URL: https://wa.genuka.com/en/docs/guides/order-notifications Language: English > Send WhatsApp order confirmations, shipping and delivery updates from your backend: utility templates, variables, idempotency and delivery statuses. To notify customers about an order on WhatsApp, create one `UTILITY`-category template per step (confirmed, shipped, delivered), get it approved by Meta, then call `POST /api/v1/messages` from your backend with the order's variables. Free-form text is only allowed if the customer messaged you within the last 24 hours. *Last updated October 8, 2026* ## What do I need before I start? * A number connected to Genuka WA and its `connectionId` — see the [quickstart](https://wa.genuka.com/en/docs/quickstart). * An API key, used **server-side only** — see [Authentication](https://wa.genuka.com/en/docs/authentication). * **A payment method on the WhatsApp Business account, at Meta.** Genuka is a Meta Tech Provider, not a BSP: Meta bills the account directly, and a client onboarded by a Tech Provider must add its own payment method ([Meta, Partners](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview)). Without one, the templates get approved but every send comes back with error `131042` — see [error 131042](https://wa.genuka.com/en/docs/errors/131042). ## Which templates should an online store create? One template for each moment the customer is waiting for news about their order. Three are enough to start: | Step | Template | Body variables | Button | | --------------- | ----------------- | --------------------------- | --------------------------------- | | Order confirmed | `order_confirmed` | first name, number, amount | URL "View my order" | | Order shipped | `order_shipped` | first name, number, carrier | URL "Track my parcel" | | Order delivered | `order_delivered` | first name, number | None: the message invites a reply | To Meta, a utility template is triggered by a customer action or request, is specific to that customer, and contains nothing promotional. Order confirmations and shipping updates are the textbook examples; a template that mixes order information with an offer, an upsell or a renewal push gets recategorized as marketing ([Meta docs, categorization](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/template-categorization)). Keep promotions out of these messages. ## How do I create a utility template? 1. ### Submit the template Every body variable needs a sample in `example.body_text`, and a URL button's variable only replaces the **end** of the address, with its own sample. ```bash title="POST /api/v1/templates" curl -X POST https://wa.genuka.com/api/v1/templates \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "name": "order_shipped", "language": "en", "category": "UTILITY", "components": [ { "type": "BODY", "text": "Hi {{1}}, your order {{2}} is on its way with {{3}}. Track it with the button below.", "example": { "body_text": [["Awa", "ORD-1042", "DHL"]] } }, { "type": "BUTTONS", "buttons": [ { "type": "URL", "text": "Track my parcel", "url": "https://shop.example.com/track/{{1}}", "example": ["https://shop.example.com/track/ORD-1042"] } ] } ] }' ``` In the dashboard: **Templates** › **New template**, Utility category. 2. ### Wait for approval Meta's verdict arrives as a `template_status` webhook (if your endpoint subscribes to `template.status_changed`, or to every event), can be read on `GET /api/v1/templates/{id}`, and can be caught up with `POST /api/v1/templates/sync` if a webhook went missing. Only send an `approved` template. 3. ### Set a time-to-live if the message goes stale If a message cannot be delivered, WhatsApp keeps retrying for the template's time-to-live: 30 days by default for a utility template, configurable from 30 seconds to 12 hours ([Meta docs, time-to-live](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/time-to-live)). A "your courier arrives in 30 minutes" received the next day does more harm than good: for that kind of template, pass `messageSendTtlSeconds` at creation. ## How do I send the notification from my backend? One call per notification. `body` fills `{{1}}`, `{{2}}`, `{{3}}` in order (`variables` is an accepted alias), and `buttons[].text` completes the button's URL. **Node.js** ```ts title="send-order-shipped.ts" const API = "https://wa.genuka.com/api/v1"; type Order = { number: string; firstName: string; phone: string; carrier: string }; /** The response body: `data` on success, otherwise `error` (plus `meta` if Meta refused). */ type ApiResult = { data?: { messageId: string }; error?: string; message?: string; meta?: { retryable?: boolean; code?: number }; }; export async function sendOrderShipped(order: Order): Promise { const response = await fetch(`${API}/messages`, { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ connectionId: process.env.GENUKA_WA_CONNECTION_ID, to: order.phone, // international, with the +: "+237699001122" template: { name: "order_shipped", language: "en", // the template's exact language, otherwise the send targets "en_US" body: [order.firstName, order.number, order.carrier], buttons: [{ type: "url", text: order.number }], }, }), signal: AbortSignal.timeout(15_000), }); // A 5xx from the CDN is not JSON: without this catch, the parse error would lose `retryable` // and the notification would be filed as a permanent failure. const result = (await response.json().catch(() => ({}))) as ApiResult; if (!response.ok || !result.data) { throw Object.assign(new Error(result.message ?? result.error), { status: response.status, // A permanent refusal (template, number) is not worth retrying: meta.retryable says so. retryable: response.status >= 500 || result.meta?.retryable === true, }); } return result.data.messageId; } ``` **Python** ```python title="send_order_shipped.py" import os import requests API = "https://wa.genuka.com/api/v1" class NotificationError(Exception): def __init__(self, status, body): super().__init__(body.get("message") or body.get("error")) # A permanent refusal (template, number) is not worth retrying: meta.retryable says so. self.retryable = status >= 500 or body.get("meta", {}).get("retryable") is True def send_order_shipped(order): response = requests.post( f"{API}/messages", headers={ "Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}", "User-Agent": "shop/1.0 (+https://shop.example.com)", }, json={ "connectionId": os.environ["GENUKA_WA_CONNECTION_ID"], "to": order["phone"], # international, with the +: "+237699001122" "template": { "name": "order_shipped", "language": "en", "body": [order["first_name"], order["number"], order["carrier"]], "buttons": [{"type": "url", "text": order["number"]}], }, }, timeout=15, ) try: body = response.json() except ValueError: # a 5xx from the CDN is not JSON body = {} if not response.ok: raise NotificationError(response.status_code, body) return body["data"]["messageId"] ``` **PHP (Laravel)** ```php title="app/Notifications/SendOrderShipped.php" timeout(15) ->post('https://wa.genuka.com/api/v1/messages', [ 'connectionId' => config('services.genuka_wa.connection_id'), 'to' => $order->phone, // international, with the +: "+237699001122" 'template' => [ 'name' => 'order_shipped', 'language' => 'en', 'body' => [$order->first_name, $order->number, $order->carrier], 'buttons' => [['type' => 'url', 'text' => $order->number]], ], ]); if ($response->failed()) { // A permanent refusal (template, number) is not worth retrying: meta.retryable says so. $retryable = $response->serverError() || $response->json('meta.retryable') === true; throw new OrderNotificationFailed($response->json('message') ?? $response->json('error'), $retryable); } return $response->json('data.messageId'); } ``` Call it from a queued job (`ShouldQueue`) rather than inside the checkout request: the customer should not wait on WhatsApp to see their confirmation page. **curl** ```bash title="POST /api/v1/messages" curl -X POST https://wa.genuka.com/api/v1/messages \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "to": "+237699001122", "template": { "name": "order_shipped", "language": "en", "body": ["Awa", "ORD-1042", "DHL"], "buttons": [{ "type": "url", "text": "ORD-1042" }] } }' ``` ```json title="Response" { "data": { "messageId": "wamid.HBg…" } } ``` A `200` means Meta accepted the message; follow delivery by webhook, using this `messageId`. To send the same template to a whole list at once, use [campaigns](https://wa.genuka.com/en/docs/campaigns) instead; every content type is described in [Sending messages](https://wa.genuka.com/en/docs/messages). ## When can I send free-form text instead of a template? When the customer messages you, a 24-hour customer service window opens, and every new message from them restarts it. While it is open you can reply with any message; once it closes, only approved templates go through ([Meta docs](https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/send-messages)). The typical case: the customer replies to the shipping update with "I won't be home tomorrow". Your answer can be plain text, quoted with `replyTo`: ```json title="POST /api/v1/messages" { "connectionId": "con_1", "to": "+237699001122", "replyTo": "wamid.HBg…", "text": "Noted, the courier will come back Thursday between 2 and 4 pm." } ``` On the bill, Meta does not charge for non-template messages, and a utility template delivered inside an open window is free too ([Meta pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing)). Outside the window, free-form text fails, but not at send time. In our tests on a live number, Meta accepted it with a `messageId` and then dropped it: usually a `failed` status with error `131047` follows ([Meta error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes)); sometimes no status arrives at all. So do not wait for the failure: Genuka WA does not block the send, but warns you in the response, and that warning is what to act on: ```json title="200 OK — with a warning" { "data": { "messageId": "wamid.HBg…", "warning": { "code": "outside_service_window", "message": "…" } } } ``` `outside_service_window` means the customer's last message is more than 24 hours old: send a template instead. `unverified_service_window` only means we have no inbound message from that contact on record, which does not prove the window is closed. Watch the message's status, and resend as a template only if `failed` with `131047` arrives, or nothing does: resending right away risks delivering the same notification twice. ## How do I avoid sending the same notification twice? The API has no idempotency key: your backend is what guarantees that an order receives each notification only once. The simplest way is a table whose primary key is the order + step pair. ```sql title="schema.sql" create table order_notifications ( order_id text not null, event text not null, -- confirmed | shipped | delivered status text not null default 'pending', message_id text unique, -- the wamid returned by Genuka WA error_code integer, created_at timestamptz not null default now(), primary key (order_id, event) ); ``` ```ts title="notify-once.ts" import { Pool } from "pg"; const db = new Pool(); export async function notifyOnce(orderId: string, event: string, send: () => Promise) { // 1. Claim: a second call for the same order and step does nothing. const claimed = await db.query( "insert into order_notifications (order_id, event) values ($1, $2) on conflict do nothing", [orderId, event], ); if (claimed.rowCount === 0) return; try { // 2. Send, 3. store the wamid: it is what links later statuses to this row. const messageId = await send(); await db.query( "update order_notifications set status = 'sent', message_id = $3 where order_id = $1 and event = $2", [orderId, event, messageId], ); } catch (error) { if ((error as { retryable?: boolean }).retryable === true) { // Release the claim: the job's next run will try again. await db.query("delete from order_notifications where order_id = $1 and event = $2", [orderId, event]); } else { await db.query( "update order_notifications set status = 'failed' where order_id = $1 and event = $2", [orderId, event], ); } throw error; } } // await notifyOnce(order.id, "shipped", () => sendOrderShipped(order)); ``` A network timeout is ambiguous: Meta may have accepted the message before the response got lost. This code files it as a failure rather than risk a duplicate. If, for your store, a rare duplicate beats a lost confirmation, treat timeouts as retryable too. ## How do I track delivery of each notification? Statuses reach your webhook with type `message_status`. `data.id` is the `messageId` returned at send time, and `data.status` is `sent`, `delivered`, `read` or `failed`. On failure, `data.errors[0].code` carries Meta's code ([Meta docs, statuses](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status)). ```ts title="In your webhook receiver (signature already verified)" const RANK: Record = { pending: 0, sent: 1, delivered: 2, read: 3, failed: 4 }; if (event.type === "message_status") { const { id: messageId, status, errors } = event.data; const row = await findNotificationByMessageId(messageId); // Statuses can arrive out of order: a status is never downgraded, // and `failed` is final. if (row && row.status !== "failed" && (RANK[status] ?? -1) > RANK[row.status]) { await setNotificationStatus(messageId, status, errors?.[0]?.code ?? null); } } ``` To Meta, `read` means the message was displayed in an open chat on the customer's device; do not build business logic that waits for it. Signature verification and de-duplication are covered in [Receive replies and statuses via webhook](https://wa.genuka.com/en/docs/guides/receive-messages-webhooks). ## Which errors will I run into most? | Error | Where to read it | Meaning | What to do | | --------------------------- | ----------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | `400 missing_fields` | Response | `connectionId` or `to` missing | Complete the request | | `400 recipient_is_sender` | Response | `to` is the sending number itself | Change the recipient | | `402 subscription_past_due` | Response | The trial or the paid period has lapsed: nothing is sent until it is renewed | Renew the plan, then resend what `notifyOnce` filed as `failed` | | `402 plan_limit_messages` | Response | The period's message allowance is used up | Add numbers or change plan | | `404 connection_not_found` | Response | The `connectionId` does not exist or is outside your key's scope | Check the id and the key | | `409 number_released` | Response | The number was released from your plan | Reconnect it through Embedded Signup | | `403 account_deactivated` | Response | The client that owns the number is suspended | Restore the client from its page in the dashboard | | `132001` | `meta.code` or `failed` status | Template missing in that language, or not approved | Check name, language and status | | `132000` | `meta.code` or `failed` status | Variable count differs from the template's | Match `body` to `{{1}}`…`{{n}}` | | `131026` | `failed` status | Message undeliverable, e.g. number without WhatsApp | Fall back to email or SMS | | `131047` | `failed` status, sometimes never received | Free-form text sent more than 24 h after the customer's last message | Send a template; act on the response's `warning` | | `130429` | `meta.code` | Cloud API throughput reached | Retry with growing backoff | | `131042` | `meta.code` or `failed` status | Payment method problem on the Meta account | The client fixes billing on Meta's side — see [error 131042](https://wa.genuka.com/en/docs/errors/131042) | The meanings of Meta's codes come from its [error code list](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes). ## FAQ ### How much does a WhatsApp order notification cost? Meta charges for delivered templates by category and recipient country, billed straight to the merchant's WhatsApp Business account; a utility template delivered inside an open customer service window is free ([Meta pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing)). Genuka WA takes no markup on those rates: you pay a subscription per number, with a message allowance — see the [plans](https://wa.genuka.com/en#pricing). ### Can I slip a promo code into the order confirmation? No. An offer or an upsell inside a utility template gets it recategorized as marketing by Meta. Send the promotion separately, in a marketing template, to customers who agreed to receive them. ### Do I need the customer's consent? Yes. Meta asks you to state clearly that the person is opting in to receive messages from your business, and to name that business ([Meta docs, opt-in](https://developers.facebook.com/documentation/business-messaging/whatsapp/getting-opt-in)). A "Get order updates from \[Store name] on WhatsApp" checkbox at checkout, stored with the order, meets both conditions. ### What if the customer has no WhatsApp account? The status comes back `failed` with code `131026`. Do not retry: send the same information by email or SMS. ### I run several stores: how do I pick the sending number? Each store connects its number and gets its own `connectionId`; that is what decides where the message comes from. A partner key reaches all your stores, a client key only one — see [Authentication](https://wa.genuka.com/en/docs/authentication) and, to find a store's number, [Connecting a number](https://wa.genuka.com/en/docs/onboarding). ## Sources * [Meta — Template categorization](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/template-categorization) * [Meta — Time-to-live](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/time-to-live) * [Meta — Send messages (customer service window)](https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/send-messages) * [Meta — Pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing) * [Meta — Status messages webhook](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status) * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Getting opt-in](https://developers.facebook.com/documentation/business-messaging/whatsapp/getting-opt-in) * [Meta — Partners (Tech Providers and Solution Partners)](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview) --- # Send a WhatsApp campaign through the API URL: https://wa.genuka.com/en/docs/guides/campaigns-api Language: English > Send a WhatsApp campaign through the API: marketing template, recipients, launch, plan quotas, Meta's marketing limits and delivery tracking. To send a WhatsApp campaign through the API with Genuka WA, you get a `MARKETING` template approved by Meta, create the campaign with `POST /api/v1/campaigns` and its recipient list, then launch it with `POST /api/v1/campaigns/{id}/launch`. Every recipient is tracked through to read, and Meta bills delivered messages directly to your own WhatsApp Business account. *Last updated October 8, 2026* ## What you need before sending a campaign * **A connected number** and its `connectionId` — see [Connecting a number](https://wa.genuka.com/en/docs/onboarding). * **An API key**, used server-side only — see [Authentication](https://wa.genuka.com/en/docs/authentication). * **A payment method on your Meta account.** Genuka is a Meta Tech Provider, not a BSP: Meta bills your WhatsApp Business Account (WABA) directly, and a client onboarded by a Tech Provider must add its own payment method ([Meta, Partners](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview)). Without one, the template gets approved but every send comes back with error `131042`. * **Recipients who agreed to hear from you.** That is a WhatsApp rule, not a best practice — the opt-in section further down covers it. ## How do I create the marketing template? Outside the 24-hour customer service window, only an approved template is delivered. A campaign always uses one. Submit it with the `MARKETING` category: ```bash title="POST /api/v1/templates" curl -X POST https://wa.genuka.com/api/v1/templates \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "name": "october_sale", "language": "en_US", "category": "MARKETING", "components": [ { "type": "BODY", "text": "Hi {{1}}, 20% off the whole shop until Sunday.", "example": { "body_text": [["Awa"]] } }, { "type": "BUTTONS", "buttons": [ { "type": "URL", "text": "See the offers", "url": "https://example.com/sale" } ] } ] }' ``` The response is a `201` carrying the template's `id` and its status. Meta reviews it — review can take up to 24 hours ([Meta, Template fundamentals](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/overview)). The verdict reaches your [webhooks](https://wa.genuka.com/en/docs/webhooks) (`template.status_changed`) and `GET /api/v1/templates/{id}`. A campaign only goes out with an `approved` template. Every body variable (`{{1}}`, `{{2}}`…) needs an example, or the template is rejected. The [API reference](https://wa.genuka.com/en/docs/api#templates) covers media headers, named parameters and buttons. ## How do I create and launch the campaign? 1. ### Create the campaign `POST /api/v1/campaigns` takes four fields, all required: `connectionId`, `templateId`, `name` and `recipients`. Each recipient carries its number in international format (`+` and country code) and its own `variables`. The campaign is created as `draft`: nothing is sent yet. 2. ### Launch it `POST /api/v1/campaigns/{id}/launch` sends every recipient still `pending` and answers with the count: `sent`, `failed`, `skipped`. `sent` means Meta accepted the message, not that it arrived — delivery and read come afterwards, by webhook. 3. ### Track the statuses Read the counters on `GET /api/v1/campaigns/{id}` and the per-recipient detail on `GET /api/v1/campaigns/{id}/recipients`. The tracking section further down covers both. **curl** ```bash # 1. Create the campaign (draft) curl -X POST https://wa.genuka.com/api/v1/campaigns \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "templateId": "tpl_123", "name": "October sale", "recipients": [ { "to": "+237690000001", "variables": ["Awa"] }, { "to": "+237690000002", "variables": ["Paul"] } ] }' # { "data": { "id": "cmp_9", "name": "October sale", "status": "draft", "_count": { "recipients": 2 } } } # 2. Launch it curl -X POST https://wa.genuka.com/api/v1/campaigns/cmp_9/launch \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" # { "data": { "sent": 2, "failed": 0, "skipped": 0 } } ``` **Node.js** ```ts const API = "https://wa.genuka.com/api/v1"; async function post(path: string, body?: unknown): Promise { const response = await fetch(`${API}${path}`, { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: body === undefined ? undefined : JSON.stringify(body), }); // A 524 from the CDN (launch running too long, see the limitations below) has no JSON body. const json = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(`${response.status} ${json.error ?? "no_json_body"}: ${json.message ?? ""}`); } return json.data as T; } const campaign = await post<{ id: string }>("/campaigns", { connectionId: "con_1", templateId: "tpl_123", name: "October sale", recipients: [ { to: "+237690000001", variables: ["Awa"] }, { to: "+237690000002", variables: ["Paul"] }, ], }); const result = await post<{ sent: number; failed: number; skipped: number }>( `/campaigns/${campaign.id}/launch`, ); console.log(result); // { sent: 2, failed: 0, skipped: 0 } ``` **Python** ```python import os import requests API = "https://wa.genuka.com/api/v1" HEADERS = { "Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}", "User-Agent": "acme-crm/1.0 (+https://example.com)", } created = requests.post(f"{API}/campaigns", headers=HEADERS, timeout=30, json={ "connectionId": "con_1", "templateId": "tpl_123", "name": "October sale", "recipients": [ {"to": "+237690000001", "variables": ["Awa"]}, {"to": "+237690000002", "variables": ["Paul"]}, ], }) created.raise_for_status() campaign_id = created.json()["data"]["id"] # Past about 2 minutes, the CDN answers 524 while sending carries on server-side: # do not launch again, follow the campaign's status (see the limitations below). launched = requests.post(f"{API}/campaigns/{campaign_id}/launch", headers=HEADERS, timeout=150) launched.raise_for_status() print(launched.json()["data"]) # {'sent': 2, 'failed': 0, 'skipped': 0} ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 150, CURLOPT_USERAGENT => "acme-crm/1.0 (+https://example.com)", CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("GENUKA_WA_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => $body === null ? "" : json_encode($body), ]); $json = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($status >= 400) { // A 524 from the CDN (launch running too long, see the limitations below) has no JSON body. throw new RuntimeException("{$status} " . ($json['error'] ?? 'no_json_body')); } return $json["data"]; } $campaign = genuka_post("/campaigns", [ "connectionId" => "con_1", "templateId" => "tpl_123", "name" => "October sale", "recipients" => [ ["to" => "+237690000001", "variables" => ["Awa"]], ["to" => "+237690000002", "variables" => ["Paul"]], ], ]); print_r(genuka_post("/campaigns/{$campaign['id']}/launch")); // sent, failed, skipped ``` `variables` is a positional array for the body parameters. For a media header, a dynamic button or named parameters, pass an object instead — `{ "body": [...], "bodyNamed": { "first_name": "Awa" }, "header": {...}, "buttons": [...] }` — the same shape as [sending a template](https://wa.genuka.com/en/docs/api#messages). A named-parameter template's values go in `bodyNamed`: put in `body`, they would be sent as positional parameters. ### Why is the launch refused? These checks run before the first send: a refused launch has sent nothing. The most common refusals: | Status | Code | Cause | | ------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `402` | `plan_limit_messages` | The list does not fit in what is left of the period's quota. Every `pending` recipient counts, including those later excluded as opted out | | `402` | `subscription_past_due` | The trial or the paid period has lapsed | | `400` | `send_template` | The template is not `approved` (in review, rejected, paused) — the message says which | | `409` | `send_config` | The number was disconnected from the API in the WhatsApp Business app ([coexistence](https://wa.genuka.com/en/docs/guides/coexistence)): reconnect it | | `409` | `number_released` | The number was released from your plan: reconnect it through Embedded Signup | | `403` | `account_deactivated` | The client that owns the number is suspended | | `404` | `campaign_not_found` | The `id` does not exist or is outside your key's scope | On creation, a `templateId` and a `connectionId` belonging to two different clients answer `400 template_connection_mismatch`, and a list without a single usable `to` answers `400 no_valid_recipients`. Other codes are in the [error reference](https://wa.genuka.com/en/docs/api#errors). ## How many messages can a campaign send? Genuka never charges for a message. Your plan does include a **quota of outbound messages per paid number per billing period**, shared between API sends and campaign recipients. The period's quota is: numbers paid for × the tier's per-number allowance. The counter starts again from zero with each new period: every month on a monthly subscription, at renewal on an annual one. | Tier | Max numbers | Messages included per number per period | | ---------- | ----------- | --------------------------------------- | | Starter | 5 | 500 | | Growth | 15 | 5,000 | | Scale | 100 | 30,000 | | Enterprise | custom | custom | Example: 3 numbers on Growth give 15,000 messages for the period. The 7-day free trial runs on the Growth tier with one number, so 5,000 messages. Only messages Meta accepted are counted: a send Meta refuses on the call consumes nothing, but a message accepted and then reported as `failed` by webhook stays counted. Prices are on the [pricing page](https://wa.genuka.com/en#pricing) and in Markdown at [/pricing.md](https://wa.genuka.com/pricing.md). ## What limits does Meta put on marketing campaigns? The Genuka quota is not the only ceiling. Meta enforces four of its own, whatever your plan: | Meta limit | What it says | What you see | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Messaging limit** | Unique recipients you can reach outside a customer service window, per moving 24 hours, at business portfolio level: 250 for a new portfolio, then 2,000, 10,000, 100,000, unlimited ([docs](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits)) | WhatsApp Manager, **Account tools > Messaging limits** (same page). The `messagingLimitTier` from `GET /api/v1/numbers?refresh=true` is only indicative: it comes from a field Meta has deprecated and can be empty | | **Per-user marketing limit** | WhatsApp may not deliver a marketing template to someone who receives many and reads few. Wait at least 24 hours before resending ([docs](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/per-user-limits/)) | Error `131049` on the status webhook, recipient `failed` | | **US numbers** | WhatsApp does not currently deliver marketing templates to US phone numbers (same page) | Recipient fails | | **Throughput** | 80 messages per second per number by default, 20 for a coexistence number ([throughput](https://developers.facebook.com/documentation/business-messaging/whatsapp/throughput), [coexistence](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users/)) | The launch is paced to it | The per-user marketing limit is not currently active for messages sent from or to the European Economic Area, the United Kingdom, Japan and South Korea (same Meta page). A campaign's `MARKETING` templates are first offered to Meta's [Marketing Messages API](https://developers.facebook.com/documentation/business-messaging/whatsapp/marketing-messages/get-started); if your account does not have access to it, the send falls back to the classic endpoint without stopping the campaign. ### How much does a campaign cost on Meta's side? Meta charges per delivered message, at the rate for the template's category and the recipient's country calling code, on your business portfolio ([Meta, Pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing)). Genuka adds no markup and never handles that payment. ## Do I need opt-in for a WhatsApp campaign? Yes. The WhatsApp Business policy only lets you contact someone who gave you their number **and** confirmed they want to receive your messages ([WhatsApp Business Messaging Policy](https://whatsappbusiness.com/policy/)). Meta adds that the request must name your business and state clearly what the person is opting in to, through any channel you like — website, SMS, paper form ([Meta, Get opt-in](https://developers.facebook.com/documentation/business-messaging/whatsapp/getting-opt-in)). Quality that stays low for a sustained period gets the number rate-limited by Meta (same page). Genuka enforces marketing opt-outs on campaigns for you: * When a user stops marketing messages from within WhatsApp, Meta announces it by webhook and Genuka records it. A send refused with `131050` ("this recipient has chosen to stop receiving marketing messages") is recorded too. * When a `MARKETING` campaign launches, those contacts are excluded **before** any send and marked `skipped`, with the reason. They consume neither quota nor Meta billing. * `UTILITY` and `AUTHENTICATION` campaigns are not filtered: they are not marketing messages. A keyword of your own ("reply STOP") is not interpreted automatically: replies reach you on `message.received`, and removing those contacts from your lists is up to you. ## How do I track a campaign's results? `GET /api/v1/campaigns/{id}` returns the campaign, its template, its number and a `stats` object counting recipients by status: ```json title="GET /api/v1/campaigns/cmp_9" { "data": { "id": "cmp_9", "name": "October sale", "status": "completed", "template": { "id": "tpl_123", "name": "october_sale", "language": "en_US", "status": "approved" }, "_count": { "recipients": 1200 }, "stats": { "read": 640, "delivered": 410, "sent": 95, "failed": 41, "skipped": 14 } } } ``` Each recipient is counted once, under its most advanced status: a read message sits in `read`, not in `delivered`. The delivery rate therefore reads `(delivered + read) / (total − skipped)`. | Status | Meaning | | ----------- | ------------------------------------------------- | | `pending` | Not sent yet | | `sent` | Accepted by Meta | | `delivered` | Reached the phone | | `read` | Read — only if the recipient has read receipts on | | `failed` | Refused, with Meta's reason in `errorMessage` | | `skipped` | Excluded at launch: opted out of marketing | For failures, `GET /api/v1/campaigns/{id}/recipients?status=failed&limit=1000` lists each recipient with `errorMessage`, `sentAt`, `deliveredAt`, `readAt` and `failedAt` (200 rows by default, 1,000 at most). In real time, subscribe an endpoint to `message.delivered`, `message.read`, `message.failed` and `user_preference.stopped` — see [Webhooks](https://wa.genuka.com/en/docs/webhooks). ### How do I resend to recipients blocked by the marketing limit? The per-user marketing limit shows up after the send: Meta accepts the message, then the status webhook comes back `failed` with error `131049` ([Meta, Per-user limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/per-user-limits/)). The recipient moves to `failed`, with Meta's text in `errorMessage` (Meta describes this error as "This message was not delivered to maintain healthy ecosystem engagement"). Calling `launch` again will not resend to them: it only picks up `pending` recipients. To try those people again, wait at least 24 hours, list `GET /api/v1/campaigns/{id}/recipients?status=failed`, keep the ones carrying that reason (or the `131049` code received on `message.failed`) and create a new campaign with them. In the rarer case where Meta refuses the send itself with `131049`, the recipient stays `pending`, and calling `launch` again 24 hours later picks it up. ## Current limitations * **The launch is synchronous and time-bound.** The call works through the list one recipient after another: each send waits for Meta's answer, then for its status to be stored, so the real rate stays well below the number's ceiling. Past about 2 minutes, the CDN in front of the API answers `524` — an error page, not JSON — while sending carries on server-side ([Cloudflare, error 524](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-524/)). Keep campaigns to a few hundred recipients and split a large audience into several campaigns. * **After a `524`, follow the campaign instead of launching it again.** Poll `GET /api/v1/campaigns/{id}` until `status` is no longer `running`. Never call `launch` on a `running` campaign: nothing prevents it today, and two runs in parallel would send the message twice to the recipients the first run has not reached yet. * **A run that goes on too long is cut off.** The server does not let a call run forever. If the campaign is still `running` 15 minutes after the call, the run was cut short: the recipients still `pending` were not sent, and calling `launch` again picks them up. * **No scheduling.** There is no date field: call `launch` when you want it to go, from your own scheduled job. ## What if I want a UI rather than an API? Genuka WA has no campaign console: `/api/v1/campaigns` is an integration primitive (template + recipients + statuses). If you want to segment a customer base and launch campaigns without writing code, [Genuka Core](https://genuka.com), Genuka's business management platform, does it: it categorizes your customers, analyzes their buying behavior and launches targeted WhatsApp, SMS and email campaigns. ## FAQ ### Can I send a campaign to contacts who never messaged me? Yes, with an approved template and their opt-in. That is exactly what a template is for: it is the only message delivered outside the 24-hour window. Meta's messaging limit (250 unique recipients per 24 hours for a new portfolio) applies to those sends. ### How much does a WhatsApp campaign cost with Genuka WA? Two separate lines. Meta bills each delivered template to your business portfolio, by category and recipient country. Genuka charges a subscription per WhatsApp number, with a message allowance included, and takes no markup on Meta's rates. ### What happens if a recipient opted out? On a `MARKETING` campaign, they are excluded before sending and marked `skipped`. They do not use your quota and Meta charges you nothing for them. ### Why is `sent` high but `delivered` low? `sent` only means Meta accepted the message. Delivery receipts follow by webhook; if they do not come, look at `failed` and at errors `131049` (marketing limit) or `131026` (recipient unreachable). ### Can I launch the same campaign again? Yes, once it is no longer `running` (`completed` or `failed`): `launch` then only sends recipients still `pending`, never a message that already went out. Do not launch it again while it is `running`, even after a `524`: wait for it to leave that status, or 15 minutes if it stays there (see the limitations above). ## Sources * Meta — [Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits) * Meta — [Per-user marketing template message limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/per-user-limits/) * Meta — [Throughput](https://developers.facebook.com/documentation/business-messaging/whatsapp/throughput) * Meta — [Pricing on the WhatsApp Business Platform](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing) * Meta — [Get opt-in for WhatsApp](https://developers.facebook.com/documentation/business-messaging/whatsapp/getting-opt-in) * Meta — [Template fundamentals](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/overview) * Meta — [Marketing Messages API: get started](https://developers.facebook.com/documentation/business-messaging/whatsapp/marketing-messages/get-started) * Meta — [Partners (Tech Providers and Solution Partners)](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview) * Meta — [Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * Meta — [Onboard WhatsApp Business app users](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users/) * WhatsApp — [Business Messaging Policy](https://whatsappbusiness.com/policy/) * Cloudflare — [Error 524: a timeout occurred](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-524/) * Genuka — [genuka.com](https://genuka.com) --- # Receive WhatsApp replies and statuses via webhook URL: https://wa.genuka.com/en/docs/guides/receive-messages-webhooks Language: English > Receive WhatsApp replies and delivery statuses on your server: register a webhook, verify the HMAC signature, deduplicate, handle retries and replay. To receive WhatsApp replies and delivery statuses, register an HTTPS URL in Genuka WA: every event arrives there as a signed `POST`. Your endpoint verifies the HMAC-SHA256 signature in the `X-Genuka-Signature` header against the raw body, stores the event once using its `id`, answers 2xx within 10 seconds, then processes it in the background. *Last updated October 8, 2026* ## How do I register a webhook endpoint? 1. ### Expose a public HTTPS URL Genuka WA refuses `http://` URLs, URLs carrying a username and password, and URLs pointing at a private or local address. To develop on your machine, go through an HTTPS tunnel. Redirects are not followed: a `3xx` response counts as a failure, so register the exact final URL. 2. ### Register it In the dashboard under **Webhooks**, or through the API: ```bash title="POST /api/v1/webhooks" curl -X POST https://wa.genuka.com/api/v1/webhooks \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.example.com/webhooks/genuka", "connectionId": "con_1", "events": ["message.received", "message.sent", "message.delivered", "message.read", "message.failed", "template.status_changed"] }' ``` Without `connectionId` or `companyId`, the endpoint covers every number your key can reach. An empty or missing `events` means "every event". `template.status_changed` brings Meta's verdict on your templates (the `template_status` event): without it, an endpoint subscribed to messages only never receives it. 3. ### Copy the secret The `201` response contains `secret` (`whsec_…`). It is only returned at creation and on rotation (`PATCH /api/v1/webhooks/{id}` with `"rotateSecret": true`): store it in your environment, for example as `GENUKA_WEBHOOK_SECRET`. A rotation takes effect immediately. 4. ### Test it `POST /api/v1/webhooks/{id}/test` sends your URL one sample of each event family and returns the HTTP status it got for each. The samples are signed with your secret, like a real delivery: it is how you test your signature check before the first real message. ## What does an event look like? Each delivery is a JSON `POST` carrying **one** event. The fields around `data` tell you which business and number it belongs to; `connection_id` is the one to reuse when you reply. ```json title="POST https://api.example.com/webhooks/genuka" { "id": "cmg7v2k0x0001…", "type": "inbound_message", "field": "messages", "created_at": "2026-10-08T09:31:07.412Z", "partner_id": "cl9x…", "company_id": "cm31…", "connection_id": "cn77…", "waba_id": "102290129340398", "phone_number_id": "106540352242922", "data": { "from": "237699001122", "id": "wamid.HBgLMjM3…", "timestamp": "1791451865", "type": "text", "text": { "body": "I won't be home tomorrow" } } } ``` For the five historical types, `data` is the **raw** object Meta sent, untouched: | `type` | What it is | `data` | | ----------------- | -------------------------------------------------------------- | --------------------- | | `inbound_message` | A message from the customer | Meta's message object | | `message_status` | `sent`, `delivered`, `read` or `failed` for a message you sent | Meta's status object | | `template_status` | A template was approved, rejected or paused | Meta's change value | | `account_update` | A change on the WhatsApp Business account | Meta's change value | | `phone_quality` | A number's quality or tier changed | Meta's change value | Newer events carry a Genuka name (`user_preference.stopped`, `template.quality_changed`, `unknown.received`…) and a normalized `data`. Always switch on `type`, and ignore what you do not handle without erroring: new types can appear. See also the [Webhooks reference](https://wa.genuka.com/en/docs/webhooks). ### How do I read a customer reply? What an `inbound_message` contains depends on `data.type` ([Meta docs, incoming messages](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages)): ```json title="data — the customer tapped a reply button (interactive message)" { "context": { "from": "237690000000", "id": "wamid.of_the_message_you_sent…" }, "from": "237699001122", "id": "wamid.HBgL…", "timestamp": "1791451865", "type": "interactive", "interactive": { "type": "button_reply", "button_reply": { "id": "yes", "title": "Confirm" } } } ``` * `type: "text"` → `data.text.body`. * `type: "interactive"` → `data.interactive.button_reply.id` or `data.interactive.list_reply.id`, the `id` you gave the button or row ([Meta docs](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/interactive)). * `type: "button"` → `data.button.payload`: a quick-reply button on a template ([Meta docs](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/button)). * `data.context.id`, when present, is the `wamid` of the message the customer is replying to. ### How do I read a delivery status? ```json title="data — failed message_status" { "id": "wamid.HBgLMjM3…", "status": "failed", "timestamp": "1791451901", "recipient_id": "237699001122", "errors": [{ "code": 131026, "title": "…" }] } ``` `data.id` is the `messageId` returned by `POST /api/v1/messages`. `errors` only appears on a failure ([Meta docs, statuses](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status)). ### Which headers come with each delivery? | Header | Contents | | ---------------------- | -------------------------------------------------------------------- | | `X-Genuka-Signature` | `t=,v1=` | | `X-Genuka-Event` | The event's `type` | | `X-Genuka-Event-Field` | The underlying Meta webhook field | | `X-Genuka-Delivery` | The delivery id, equal to the body's `id`, identical across attempts | | `X-Genuka-Webhook-Id` | The endpoint that was targeted | | `X-Genuka-Attempt` | The attempt number, starting at 1 | | `X-Genuka-Test` | `true` on a test event, absent otherwise | ## How do I verify the HMAC signature? The algorithm, exactly as Genuka WA signs: 1. Read the **raw body**, before any `JSON.parse`. 2. From `X-Genuka-Signature`, read `t` (Unix seconds) and every `v1` (there can be several). 3. Reject if `t` is more than 300 seconds away from your clock. 4. Compute the hex HMAC-SHA256 of the string `t` + `.` + raw body, keyed with the **whole** secret, `whsec_` prefix included. 5. Accept if one of the `v1` values equals it, compared in constant time. > [!NOTE] > **A late redelivery still passes the window** > > Every attempt is signed as it leaves, with a fresh `t`. A retry six hours later, or a manual > replay, is therefore accepted by the 300-second tolerance; a request captured and replayed by a > third party is not, because `t` cannot be edited without invalidating `v1`. The verification function, with no dependency: ```ts title="verify-signature.ts" import crypto from "node:crypto"; const TOLERANCE_SECONDS = 300; export function verifySignature(rawBody: string, header: string | null, secret: string): boolean { if (!header) return false; let timestamp = Number.NaN; const signatures: string[] = []; for (const part of header.split(",")) { const [key, value] = part.split("=", 2).map((s) => s.trim()); if (key === "t") timestamp = Number(value); if (key === "v1" && value) signatures.push(value); } if (!Number.isFinite(timestamp) || signatures.length === 0) return false; // The timestamp is part of the HMAC: outside the window, a captured request is worthless. if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false; const expected = crypto .createHmac("sha256", secret) // the whole secret, whsec_ prefix included .update(`${timestamp}.${rawBody}`) .digest("hex"); // timingSafeEqual throws on two buffers of different byte lengths, and the header is written // by whoever sends the request: compare the byte lengths, not the string lengths ("é" is one // character but two bytes), or a forged header turns a 401 into a 500. const expectedBytes = Buffer.from(expected); return signatures.some((candidate) => { const candidateBytes = Buffer.from(candidate); return ( candidateBytes.length === expectedBytes.length && crypto.timingSafeEqual(candidateBytes, expectedBytes) ); }); } ``` Then the endpoint, for your framework: **Next.js** ```ts title="app/api/webhooks/genuka/route.ts" import { after } from "next/server"; import { verifySignature } from "@/lib/verify-signature"; export async function POST(request: Request) { const raw = await request.text(); // the body exactly as it was signed const signature = request.headers.get("x-genuka-signature"); if (!verifySignature(raw, signature, process.env.GENUKA_WEBHOOK_SECRET!)) { return new Response("invalid signature", { status: 401 }); } // A test event is signed like a real one: it must not write anything to your database. if (request.headers.get("x-genuka-test") === "true") return new Response(null, { status: 204 }); const event = JSON.parse(raw); // Store first (fast, de-duplicated on event.id), process after the response. const fresh = await saveEventOnce(event); if (fresh) after(() => processEvent(event)); return new Response(null, { status: 204 }); } ``` **Express** ```ts title="server.ts" // Run with tsx (`npx tsx server.ts`), which resolves ./verify-signature.js to the .ts file. import express from "express"; import { verifySignature } from "./verify-signature.js"; const app = express(); // Declared BEFORE app.use(express.json()): a global parser would consume the body, // and the signature would never match. app.post("/webhooks/genuka", express.raw({ type: "application/json" }), async (req, res) => { const raw = req.body.toString("utf8"); const signature = req.get("X-Genuka-Signature") ?? null; if (!verifySignature(raw, signature, process.env.GENUKA_WEBHOOK_SECRET!)) { return res.status(401).send("invalid signature"); } // A test event is signed like a real one: it must not write anything to your database. if (req.get("X-Genuka-Test") === "true") return res.sendStatus(204); const event = JSON.parse(raw); const fresh = await saveEventOnce(event); // INSERT … ON CONFLICT (id) DO NOTHING res.sendStatus(204); // answer first if (fresh) await queue.add("genuka-event", event); // process afterwards, outside the request }); app.use(express.json()); // the rest of your API ``` **Python (Flask)** ```python title="app.py" import hashlib import hmac import json import os import time from flask import Flask, abort, request app = Flask(__name__) SECRET = os.environ["GENUKA_WEBHOOK_SECRET"].encode() # the whole secret, whsec_ prefix included TOLERANCE_SECONDS = 300 def verify_signature(raw, header): if not header: return False timestamp, signatures = None, [] for part in header.split(","): key, _, value = part.partition("=") key, value = key.strip(), value.strip() # isascii(): isdigit() alone accepts "²", on which int() raises. if key == "t" and value.isascii() and value.isdigit(): timestamp = int(value) elif key == "v1" and value: signatures.append(value) if timestamp is None or not signatures: return False # The timestamp is part of the HMAC: outside the window, a captured request is worthless. if abs(time.time() - timestamp) > TOLERANCE_SECONDS: return False expected = hmac.new(SECRET, f"{timestamp}.".encode() + raw, hashlib.sha256).hexdigest() return any(hmac.compare_digest(s.encode(), expected.encode()) for s in signatures) @app.post("/webhooks/genuka") def genuka_webhook(): raw = request.get_data() # the bytes received, before any parsing if not verify_signature(raw, request.headers.get("X-Genuka-Signature")): abort(401) # A test event is signed like a real one: it must not write anything to your database. if request.headers.get("X-Genuka-Test") == "true": return "", 204 event = json.loads(raw) if save_event_once(event): # INSERT … ON CONFLICT (id) DO NOTHING enqueue(event) # Celery, RQ…: processing does not hold the response return "", 204 ``` **@genuka/whatsapp** ```ts title="app/api/webhooks/genuka/route.ts" import { verifySignature } from "@genuka/whatsapp/webhooks"; export async function POST(request: Request) { const raw = await request.text(); const ok = await verifySignature( process.env.GENUKA_WEBHOOK_SECRET!, raw, request.headers.get("x-genuka-signature"), ); if (!ok) return new Response("invalid signature", { status: 401 }); // … same as the Next.js tab from here return new Response(null, { status: 204 }); } ``` The [library](https://wa.genuka.com/en/docs/library)'s `verifySignature` is asynchronous (WebCrypto, so it also runs on an edge runtime) and requires `@genuka/whatsapp` 0.1.1 or later. ## How do I answer fast enough to avoid redeliveries? A delivery succeeds on any `2xx`. Everything else fails and will be retried: a `4xx` or `5xx` status, a redirect, or a response that takes longer than **10 seconds**. The sequence that holds under load: 1. verify the signature; 2. store the event, de-duplicated on its `id` — one insert, a few milliseconds; 3. answer `204`; 4. process afterwards, in `after()`, a job queue or a worker. Only answer `5xx` if storing itself failed: that is the one case where you need Genuka WA to try again. Processing that fails after the response is replayed from your table, not from the webhook. ## How do I de-duplicate events? The body's `id` — equal to the `X-Genuka-Delivery` header — is identical across attempts and on a manual replay. It is your idempotency key. ```sql title="schema.sql" create table genuka_events ( id text primary key, -- the body's id, stable across attempts type text not null, payload jsonb not null, received_at timestamptz not null default now(), processed_at timestamptz -- null until processing succeeds ); ``` ```ts title="save-event-once.ts" export async function saveEventOnce(event: { id: string; type: string }): Promise { const result = await db.query( "insert into genuka_events (id, type, payload) values ($1, $2, $3) on conflict (id) do nothing", [event.id, event.type, JSON.stringify(event)], ); return result.rowCount === 1; // false: already received, nothing to do } ``` Two more points: * **Several endpoints, several `id`s.** If two of your endpoints cover the same number, each gets its own delivery, with its own `id`. In that case also de-duplicate on the business key: `data.id` (the `wamid`) for an inbound message, the `data.id` + `data.status` pair for a status. * **Test events** have an `id` starting with `test_`, the `X-Genuka-Test: true` header and no `X-Genuka-Delivery` header. Filter them out before writing anything to your database, as the samples above do. Finally, order is not guaranteed: a `read` can arrive before the `delivered` for the same message, and `delivered` may never arrive at all. When the customer has the chat open as the message lands, Meta treats it as delivered and read at once and sends only `read` ([Meta docs, statuses](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status)). So treat `read` as implying `delivered`, and never move a status backwards — `sent` → `delivered` → `read`, and `failed` is final. ## What happens if my server is down? Genuka WA makes up to six attempts in total: | Attempt | When | | ------- | ------------------------------------------------------------ | | 1 | As soon as the event arrives | | 2 | At least 1 minute after the previous one failed | | 3 | At least 5 minutes later | | 4 | At least 30 minutes later | | 5 | At least 2 hours later | | 6 | At least 6 hours later, then the delivery is marked `failed` | Retries leave from a queue that is drained every 10 minutes, so an attempt can arrive up to about ten minutes after its minimum delay, longer under a heavy backlog. Once your endpoint is back, find what failed and replay it: ```bash title="List failed deliveries, then replay one" curl "https://wa.genuka.com/api/v1/webhooks/wh_1/deliveries?status=failed" \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" curl -X POST https://wa.genuka.com/api/v1/webhooks/wh_1/deliveries/cmg7v2k0x0001/replay \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" ``` The replay answers `202` and starts over with a fresh attempt budget. A delivery still `pending` is refused (`409 delivery_pending`): it is already in the queue. The log keeps the exact body of each delivery; how long it is kept depends on your plan and is reported in `meta.retentionDays`. The dashboard's **Webhooks** section offers the same log and the same resend button. ## How do I reply to a message I received? As long as the customer wrote to you within the last 24 hours, you can answer with free-form text ([Meta docs](https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/send-messages)). Reuse the event's `connection_id`, put a `+` in front of `data.from`, and quote the message with `replyTo`: ```json title="POST /api/v1/messages" { "connectionId": "cn77…", "to": "+237699001122", "replyTo": "wamid.HBgLMjM3…", "text": "Thanks, noted!" } ``` To show the blue ticks, call `POST /api/v1/messages/{wamid}/read` with `{ "connectionId": … }` in the body — the `wamid` URL-encoded. Outside the window you need a template: see [Order notifications](https://wa.genuka.com/en/docs/guides/order-notifications). ## FAQ ### Why does my signature never match? Almost always one of four causes: the body was parsed and re-serialized before verification (a global `express.json()`, for instance); the secret was copied without its `whsec_` prefix; the server clock drifts by more than five minutes; or the secret has been rotated since. ### Can I receive Meta's webhooks directly? No. Cloud API webhooks reach Genuka WA, which relays them to you signed with your secret. For the five historical types, `data` stays Meta's original object: code that already reads Meta's format feels at home. ### Do I need an API key to receive webhooks? No. The only thing to check on the receiving end is the signature. Never process an unsigned event. ### Do statuses arrive in order? No: `read` can come before `delivered`, or even arrive alone. Rank statuses, treat `read` as implying `delivered`, and never move them backwards. ## Sources * [Meta — Messages webhook reference](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages) * [Meta — Interactive message replies](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/interactive) * [Meta — Button message replies](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/button) * [Meta — Status messages webhook](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status) * [Meta — Send messages (customer service window)](https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/send-messages) --- # Coexistence: WhatsApp API and the Business app URL: https://wa.genuka.com/en/docs/guides/coexistence Language: English > Keep the WhatsApp Business app on your number while using the API: requirements (2.24.17+), onboarding steps, what syncs and the limits. Yes, you can use the WhatsApp API without leaving the WhatsApp Business app: that is Meta's coexistence. With the app on version 2.24.17 or later, you connect the same number to Genuka WA, keep replying by hand from the phone and send at volume through the API. New messages in one-to-one conversations show up on both sides. *Last updated October 8, 2026* ## Can my number use coexistence? It depends on which app the number runs on today. Meta does not register a number already in use on WhatsApp on the Cloud API unless it is deleted first ([Meta, Business phone numbers](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/phone-numbers)); coexistence is the exception built for the WhatsApp Business app ([Meta, Onboard WhatsApp Business app users](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users/)). | Your number today | What to do | | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | On the **WhatsApp Business** app | Delete nothing. Update the app and connect the number: Meta offers coexistence during signup, the app keeps working and your conversations are kept. | | On **WhatsApp** (the regular app, not Business) | Free it first: delete the WhatsApp account on that number, or connect another number. Coexistence only covers the WhatsApp Business app. | | On no WhatsApp app | Nothing to free: it is registered directly on the Cloud API, with no app. | > [!WARNING] > **Never delete a WhatsApp Business account to connect** > > Deleting the Business app account destroys its history, and it is pointless: coexistence exists > precisely so that number can stay as it is. ## What you need before you start * **A Facebook account.** Meta asks you to sign in with it; a personal account is fine. * **The WhatsApp Business app on version 2.24.17 or later** ([Meta](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users/)), on the phone that holds the number, **within reach**: the connection is confirmed from the app. * To send templates afterwards, **a payment method on your WhatsApp Business account**: Genuka is a Tech Provider, so Meta bills your account directly ([Meta, Partners](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview)). ## How do I connect a coexistence number to Genuka WA? 1. ### Open the connect link From your dashboard, on the **Numbers** page (or **Connect link** if you manage clients), or from your public link `/connect/{your-slug}` — see [Connecting a number](https://wa.genuka.com/en/docs/onboarding). Genuka first shows a short checklist: Facebook account, number on WhatsApp Business (not regular WhatsApp), phone in hand with the app up to date. Tick all three, then click **I'm ready — open Meta**. 2. ### Pick your existing WhatsApp Business app In Meta's window, sign in with Facebook and pick or create the business portfolio. Genuka opens Embedded Signup with the WhatsApp Business app onboarding option, so Meta offers to connect your existing account rather than create a new one. Enter the number: Meta's window then waits for a verification code. 3. ### Confirm from the phone In the WhatsApp Business app, a message arrives from the official "Facebook Business" account. Tap **Connect**, then **Connect to the Business Platform**, then **Confirm** — that is the step where Meta offers to share your chat history (Genuka WA does not import it yet, see below). Copy the verification code. 4. ### Finish in Meta's window Paste the code and complete the flow. Back on Genuka, the connection is finalized: the account and the number appear in your workspace, the WhatsApp account is subscribed to webhooks, and the number is not re-registered on the Cloud API — Meta registers it itself under coexistence. Once connected, call `GET /api/v1/numbers?refresh=true` once. Genuka then reads the number from Meta and records its coexistence mode; without that call, the default list (`"source": "database"`) still shows `coexistence: false` for a number that has just connected. ```bash title="GET /api/v1/numbers?refresh=true" curl "https://wa.genuka.com/api/v1/numbers?refresh=true" \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" ``` ```json title="Response (excerpt)" { "data": [ { "id": "con_1", "displayPhoneNumber": "+237 6 90 00 00 01", "platformType": "SMB_APP", "coexistence": true, "messagesPerSecond": 20, "status": "connected", "metaStatus": "CONNECTED", "refreshed": true } ], "source": "meta" } ``` The full response also carries, for each number, the health `alerts` detected during the read, and a `limits` object with the reference throughputs. A refresh queries Meta number by number and stops at 25 numbers per call: beyond that, narrow it with `?companyId=`. ## Is my app history imported? Not for now. Meta only sends the app's contacts and history if the provider asks for them, within the 24 hours after you connect; past that, the number has to be disconnected and the flow run again ([Meta](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users/)). Genuka WA does not make that request yet. In practice: * **Your past conversations stay in the app**, untouched, but do not show up in Genuka WA. Meta allows importing one-to-one conversations from the last 180 days, without groups (same page); Genuka WA does not do it today. * **Everything after you connect does come through**: messages received on `message.received`, messages sent through the API, and the ones you type in the app (see below). * **Contacts added or changed in the app afterwards** are announced by Meta by webhook (same page); Genuka relays them on `coexistence.contacts_synced` — see [Webhooks](https://wa.genuka.com/en/docs/webhooks). ## What syncs, and what does not? Per Meta's coexistence page, with what Genuka WA does with it today: | Feature | In the app after connecting | Through the API | | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | One-to-one chats | Keep working; editing or revoking a message is supported | New messages mirrored both ways; earlier history not imported yet | | Contacts | Unchanged | No initial import; later additions and changes are announced by Meta | | Groups | Unchanged | Not synced | | Disappearing messages | Turned off in one-to-one chats | No | | View once messages | Disabled in one-to-one chats | No | | Live location | Disabled in one-to-one chats | No | | Broadcast lists | Disabled; existing lists become read-only | No | | Voice and video calls | Unchanged | No | | Catalog, orders, status | Unchanged | No | | Quick replies, labels, away and greeting messages | Unchanged | No | | Linked devices (WhatsApp Web, etc.) | Unlinked when you connect, to be relinked afterwards; WhatsApp for Windows and for WearOS are not supported | — | ## What changes for API sends? * **Throughput is fixed at 20 messages per second** on a coexistence number, against 80 by default on the Cloud API ([Meta, coexistence](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users/), [Meta, Throughput](https://developers.facebook.com/documentation/business-messaging/whatsapp/throughput)). Genuka paces your [campaigns](https://wa.genuka.com/en/docs/guides/campaigns-api) to it once the number is recorded as coexistence (the `?refresh=true` call above). A launch is also time-bound: keep campaigns to a few hundred recipients (see the limitations in the campaigns guide). * **Messages sent from the app stay free; messages sent through the API follow Cloud API pricing** ([Meta](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users/)). * **What you type on the phone shows up in Genuka.** Meta forwards those messages (the "echoes"); Genuka files them on the conversation, labeled "Sent from the WhatsApp Business app". They use nothing from your plan and are left out of delivery statistics, which only count what the API sent: no delivery receipt ever follows them. Webhook subscription: `coexistence.message_echoed`. * **Meta's limits apply as anywhere else**: approved templates outside the 24-hour window, the business portfolio's messaging limit, the per-user marketing limit. ## How do I disconnect the API from the app? Disconnecting happens in the app, not through the API: **Settings > Account > Business Platform**, then **Disconnect Account** (labels as in Meta's documentation). Meta then sends an `account_update` webhook carrying the `PARTNER_REMOVED` event ([Meta](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users/)). On Genuka, the number then moves to `disconnected` and the API can no longer send from it. Nothing is deleted: messages, templates and statistics stay attached to the number. Going through the same connect link again puts it back in service on the same record, without using a new slot in your plan. ## FAQ ### Do I have to delete my WhatsApp Business account to use the API? No. That is the whole point of coexistence: the app keeps working on the same number. Only a number used on regular WhatsApp has to be freed before connecting. ### Do messages sent from the phone count against my Genuka plan? No. Only sends made through the API or a campaign use your plan's message allowance. Messages typed in the app are synced so the conversation is complete, without being counted. ### What if my app is older than 2.24.17? Update it before opening Meta's window. Meta requires version 2.24.17 or later to offer coexistence. ### Can I keep using WhatsApp Web? Yes, once you relink it: linked devices are unlinked when you connect and can be linked again, except WhatsApp for Windows and for WearOS. ### How many messages per second can I send under coexistence? 20, whatever your tier at Genuka or at Meta. It is a ceiling Meta sets for numbers shared with the app. ## Sources * Meta — [Onboard WhatsApp Business app users](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users/) * Meta — [Business phone numbers](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/phone-numbers) * Meta — [Throughput](https://developers.facebook.com/documentation/business-messaging/whatsapp/throughput) * Meta — [Partners (Tech Providers and Solution Partners)](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview) * Genuka — [SDK coexistence guide](https://wa.genuka.com/sdk/coexistence) --- # WhatsApp API without Meta verification: what works URL: https://wa.genuka.com/en/docs/guides/without-meta-verification Language: English > WhatsApp API without Meta verification: what an unverified business can send, what verification unlocks, and the real risk of unofficial APIs. Yes, you can send messages through the official WhatsApp API without getting your business verified by Meta. Through a Tech Provider like Genuka, you connect your number in minutes and send marketing and utility templates, up to 250 unique recipients per 24 hours to start with. One-time passcodes and higher limits come, in practice, through verification. *Last updated October 8, 2026* ## Which "Meta verification" are we talking about? The phrase covers two different processes, and only one of them concerns you. | Process | Who does it | Needed to get started? | | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Becoming a provider** on the WhatsApp Business Platform (Tech Provider or Solution Partner) | The provider. Genuka is a Meta Tech Provider: the Meta app and its permissions are Genuka's | No. You connect your number through Meta's Embedded Signup, with no provider application to file | | **Verifying your business** (Business Verification) | You, on your Meta business portfolio | No. Meta made it optional at the start in 2022: you message customers right after signup and verify when you are ready to scale ([Meta changelog, April 14, 2022](https://developers.facebook.com/documentation/business-messaging/whatsapp/changelog)) | With Embedded Signup, your business owns all of its WhatsApp assets — account, number, templates ([Meta, Embedded Signup](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/overview/)). Genuka does not rent you access: it gives you an API on top of your own account. ## What can an unverified business do? | Capability | Unverified business | Verified business | | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | Reply inside the 24-hour customer service window | Yes, outside the messaging limit | Yes | | `MARKETING` and `UTILITY` templates | Yes | Yes | | `AUTHENTICATION` templates (OTP codes) | Refused at creation (Genuka observation, detailed below) | Yes | | Unique recipients outside the window, per moving 24 hours | 250 to start ([Meta](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits)) | 2,000, then raised automatically up to unlimited (same page) | | Registered numbers per business portfolio | 2 to start ([Meta](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/phone-numbers)) | Up to 20 (same page) | | Templates per WhatsApp account | 250 ([Meta](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/overview)) | Up to 6,000, with an approved display name (same page) | | Official Business Account badge | No | Possible, with other conditions ([Meta](https://developers.facebook.com/documentation/business-messaging/whatsapp/official-business-accounts/)) | The 250 limit counts **unique** recipients reached **outside** a customer service window, and it applies to the whole business portfolio, not to one number. Replying to a customer who just wrote to you does not eat into it. ## How do I send a first message without verification? 1. ### Create a Genuka WA account Seven-day free trial, no card required. Your workspace opens straight away. 2. ### Connect your number From the **Numbers** page of your dashboard (or **Connect link** if you manage clients), start the connection and follow Meta's Embedded Signup — see [Connecting a number](https://wa.genuka.com/en/docs/onboarding). If the number already runs on the WhatsApp Business app, keep it: that is [coexistence](https://wa.genuka.com/en/docs/guides/coexistence). 3. ### Add a payment method on Meta's side A client onboarded by a Tech Provider adds its own payment method to its WhatsApp Business account, and Meta bills it directly ([Meta, Partners](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview)). Without one, your templates get approved but every send fails with `131042` ("there was an error related to your payment method", [Meta error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes)). 4. ### Get a utility or marketing template approved Create it with `POST /api/v1/templates` — see the [API reference](https://wa.genuka.com/en/docs/api#templates). Meta's review can take up to 24 hours ([Meta](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/overview)). 5. ### Send it One call, whatever the language: **curl** ```bash curl -X POST https://wa.genuka.com/api/v1/messages \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "to": "+237690000001", "template": { "name": "order_confirmation", "language": "en_US", "variables": ["Awa", "ORD-1042"] } }' # { "data": { "messageId": "wamid.HBg…" } } ``` **Node.js** ```ts const response = await fetch("https://wa.genuka.com/api/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ connectionId: "con_1", to: "+237690000001", template: { name: "order_confirmation", language: "en_US", variables: ["Awa", "ORD-1042"] }, }), }); const json = await response.json(); if (!response.ok) throw new Error(`${response.status} ${json.error}: ${json.message ?? ""}`); console.log(json.data.messageId); // wamid.HBg… ``` **Python** ```python import os import requests response = requests.post( "https://wa.genuka.com/api/v1/messages", headers={ "Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}", "User-Agent": "acme-crm/1.0 (+https://example.com)", }, json={ "connectionId": "con_1", "to": "+237690000001", "template": {"name": "order_confirmation", "language": "en_US", "variables": ["Awa", "ORD-1042"]}, }, timeout=30, ) response.raise_for_status() print(response.json()["data"]["messageId"]) # wamid.HBg… ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_USERAGENT => "acme-crm/1.0 (+https://example.com)", CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("GENUKA_WA_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "connectionId" => "con_1", "to" => "+237690000001", "template" => ["name" => "order_confirmation", "language" => "en_US", "variables" => ["Awa", "ORD-1042"]], ]), ]); $json = json_decode(curl_exec($ch), true); curl_close($ch); echo $json["data"]["messageId"] ?? $json["error"]; ``` A `200` means Meta accepted the message; delivery follows on your [webhooks](https://wa.genuka.com/en/docs/webhooks). Your messaging limit shows in WhatsApp Manager, under **Account tools > Messaging limits** ([Meta](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits)). `GET /api/v1/numbers?refresh=true` also returns a `messagingLimitTier` per number, as an indication only: it comes from a field Meta has deprecated and can be empty. ## What does business verification unlock? ### One-time passcodes (authentication templates) This is the limit people find out about last. On the WhatsApp accounts connected to Genuka WA, we observe that: * creating an `AUTHENTICATION` template for an unverified business fails with the Graph message "Application does not have permission for this action". The message blames the app; the cause is the client's business; * the account's health status then carries error `141010`, "The Business has not passed business verification" (`health_status` field, [Meta docs](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/health-status)); * `MARKETING` and `UTILITY` templates on the same account go through normally. That observation covers unverified businesses still at the 250 limit. The rule as 360dialog, another Meta partner, documents it is broader: complete one of Meta's scaling paths (verification by Meta or by the partner, for example) and have a messaging limit of at least 2,000 ([360dialog, Authentication messages](https://docs.360dialog.com/docs/resources/authentication-messages)). Business verification is the most direct path, not the only one. The WhatsApp account's `business_verification_status` field says so too ([Meta reference](https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/whatsapp-business-account-api)). With Genuka you never handle a Meta token: check the verification state in Meta Business Suite, on the portfolio that owns your WhatsApp account. Once the business is verified, the [OTP from Node.js guide](https://wa.genuka.com/en/docs/guides/whatsapp-otp-nodejs) takes over. ### Higher limits Verifying the business is one of the three paths Meta offers to go from 250 to 2,000 unique recipients per 24 hours. Beyond that, the limit rises on its own (10,000, 100,000, then unlimited) as long as your messages stay high quality and you use at least half of your limit over 7 days ([Meta, Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits)). Verification also takes you from 2 to 20 registered numbers on the portfolio; Meta raises that cap the same way once your portfolio reaches the 2,000 messaging limit ([Meta, Business phone numbers](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/phone-numbers)). ### Can I go past 250 without verification? Yes. Meta accepts another path: deliver 2,000 messages outside the customer service window to unique recipients over a moving 30 days, using templates with a high quality rating (same page). With a limit of 250 per 24 hours, that takes at least 8 days of near-maximum sending. Meta also lists a third path: having the partner who onboarded you verify your business. Verification itself starts in Meta Business Suite, on the portfolio that owns your WhatsApp account: follow [Meta's official procedure](https://www.facebook.com/business/help/2058515294227817). ## What about unofficial WhatsApp APIs (QR code, WhatsApp Web)? Some services promise a "WhatsApp API without Meta": you scan a QR code as you would for WhatsApp Web, and their server drives your ordinary WhatsApp account. There is indeed no Meta onboarding, no verification and no template. But: * **It breaks WhatsApp's terms.** They forbid bulk messaging and auto-messaging, unauthorized access to the services, including through automated means, and building software or APIs that work substantially like WhatsApp's services for third parties ([WhatsApp Terms of Service](https://www.whatsapp.com/legal/terms-of-service)). * **The number can be suspended at any time.** WhatsApp may suspend or terminate access that violates its terms (same page), and the WhatsApp Business policy explicitly targets messaging people at scale in an unauthorized manner ([WhatsApp Business Messaging Policy](https://whatsappbusiness.com/policy/)). The number at stake is often the one your customers know. * **The official platform's tools are missing.** No approved templates to re-engage a customer outside the 24-hour window, no marketing opt-out signal forwarded by Meta. The detailed comparison lives at [Unofficial WhatsApp APIs](https://wa.genuka.com/en/docs/compare/unofficial-whatsapp-apis). ## Which path should I choose? | What you need | The path | | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | Notifications, confirmations, reminders, campaigns: up to 250 unique recipients a day outside the window | Cloud API through Genuka, no verification | | Sending OTP codes | Cloud API through Genuka, once you clear one of Meta's scaling paths (business verification, in practice) | | Reaching more than 250 people a day outside the window from day one | Business verification | | Keeping your WhatsApp Business app on the same number | [Coexistence](https://wa.genuka.com/en/docs/guides/coexistence) | | Automating a personal WhatsApp account | No path that complies with WhatsApp's terms | ## FAQ ### Can I send OTP codes without business verification? Not until your portfolio has cleared one of Meta's scaling paths. An unverified business at the 250 limit is refused the authentication template that carries the code, with error `141010` in the account's health status. Business verification is the most direct path, not the only one. Marketing and utility templates stay available in the meantime. ### Why does Meta answer "Application does not have permission for this action"? On an `AUTHENTICATION` template, it is the symptom of an unverified business, not of a token or app problem. Genuka then returns `403 meta_rejected`, with Meta's message and its identifiers (`meta.code`, `meta.traceId`). Check the verification status in Meta Business Suite, on the portfolio that owns your WhatsApp account. If you have your own Graph API access, the WhatsApp account's `business_verification_status` and `health_status` fields say so too. ### Do I need to become a Tech Provider myself to use the API? No. Genuka is a Tech Provider: you connect your number through Meta's Embedded Signup and call Genuka's REST API with an API key, with no Meta token to manage. ### How long does it take to go from 250 to 2,000 recipients without verification? At least 8 days: you need 2,000 messages delivered outside the window to unique recipients over 30 days, with high-quality templates, and the starting limit is 250 per 24 hours. ### Can my number get banned on the official API? Not for using the API as such. Meta does rate-limit a number whose quality stays low ([Meta, Get opt-in](https://developers.facebook.com/documentation/business-messaging/whatsapp/getting-opt-in)) and restricts an account that violates its policies (errors `368` / `131031`). Message people who agreed to hear from you. ## Sources * Meta — [WhatsApp Business Platform changelog](https://developers.facebook.com/documentation/business-messaging/whatsapp/changelog) * Meta — [Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits) * Meta — [Business phone numbers](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/phone-numbers) * Meta — [Template fundamentals](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/overview) * Meta — [Official Business Accounts](https://developers.facebook.com/documentation/business-messaging/whatsapp/official-business-accounts/) * Meta — [Embedded Signup](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/overview/) * Meta — [Partners (Tech Providers and Solution Partners)](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview) * Meta — [Health status](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/health-status) * Meta — [WhatsApp Business Account API](https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/whatsapp-business-account-api) * Meta — [Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * Meta — [Get opt-in for WhatsApp](https://developers.facebook.com/documentation/business-messaging/whatsapp/getting-opt-in) * Meta — [Verify your business in Meta Business Suite](https://www.facebook.com/business/help/2058515294227817) * 360dialog — [Authentication messages](https://docs.360dialog.com/docs/resources/authentication-messages) * WhatsApp — [Terms of Service](https://www.whatsapp.com/legal/terms-of-service) * WhatsApp — [Business Messaging Policy](https://whatsappbusiness.com/policy/) --- # How much does a WhatsApp message cost in Africa? (2026) URL: https://wa.genuka.com/en/docs/guides/whatsapp-pricing-africa Language: English > How much a WhatsApp Business message costs in Africa in 2026: Meta's rates by country and category, in USD, EUR and CFA francs, with a worked example. *Last updated October 8, 2026 — Meta rates in effect since October 1, 2026.* Meta charges for every delivered WhatsApp Business message, by category and by the recipient's country. In Cameroon, Côte d'Ivoire, Senegal, Kenya and the 36 other "Rest of Africa" countries, a marketing message costs $0.0225 (€0.0186, about 12.20 CFA francs); a utility, authentication or service message costs $0.004 (€0.0033, about 2.16 CFA francs). The first 1,000 service messages on each number stay free every month. Meta bills all of it directly to your own WhatsApp Business Account (WABA): Genuka WA, a Meta Tech Provider, adds no markup and charges a separate subscription per number. ## How does Meta charge for WhatsApp messages in 2026? Since July 1, 2025, Meta charges **per message**, and only when the message is **delivered**. The price depends on two things: the message's category and the country calling code of the recipient's number. Messages your customers send you are never charged. Two rules changed on October 1, 2026. Service messages — your free-form replies, outside templates — are paid again, after being free since November 1, 2024. And utility templates sent inside the 24-hour window the customer opened, free since July 1, 2025, are now charged too. On the same day, Morocco left the "Rest of Africa" group for a higher rate, and South Africa's utility and authentication rates went up. | Category | What it is | When you can send it | Charged by Meta | | ------------------- | ----------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------ | | Marketing | Promotional template: offer, abandoned-cart reminder, new arrival | Any time | Every delivered message | | Utility | Transactional template: order confirmation, delivery update | Any time | Every delivered message, inside the 24-hour window too | | Authentication | One-time code (OTP) template | Any time | Every delivered message | | Service | Free-form message (text, image, buttons…) replying to a customer | Only inside the 24-hour window | Beyond 1,000 per number per month | | Meta Business Agent | Reply produced by Meta's own AI agent | Only inside the 24-hour window | Per token: $2 per million tokens | A free-form message sent through the Genuka WA API is a service message, whether a person wrote it or your own AI generated it: Meta files every non-template message that does not go through its Meta Business Agent platform under "service". Two exceptions: AI Providers are charged differently (Meta's "AI Providers" pricing policy), and eligible government and non-profit organizations keep their service messages free beyond the free tier through December 31, 2027. > [!WARNING] > **You pay for the category Meta applied** > > Meta charges the template's category at the time it is sent, and can change that category after > approval: a template approved as utility but judged to be marketing is recategorized through an > automatic category update. Review your templates' categories after every approval, then > regularly: a template filed as marketing costs more than five times a utility one in "Rest of > Africa". ## Which WhatsApp messages are still free? * **Messages you receive** from customers, however many. * **The first 1,000 service messages on each number, every month.** The count resets on the 1st of the month at midnight in the WhatsApp account's timezone, and unused messages do not roll over. * **Everything sent inside a free entry point window.** When a customer messages you from a Click to WhatsApp ad on the Android or iOS app and you reply within 24 hours, this window opens and can stay open for up to 7 days. Marketing, utility, authentication and service messages are not charged while it is open. * **A message that is not delivered** is not charged. > [!WARNING] > **No payment method, nothing past the free tier** > > If your WhatsApp Business Account has no payment method, Meta delivers the month's 1,000 free > service messages and nothing after that. Template sends fail with Meta error > [`131042`](https://wa.genuka.com/en/docs/errors/131042), which is about the payment method. Add a card in > [Meta's Billing Hub](https://business.facebook.com/latest/billing_hub/payment_methods/) before > your first send. ## How much does a WhatsApp message cost in each African country? Meta publishes one rate card per currency. The first table is Meta's euro rate card, converted to CFA francs at the fixed parity of €1 = 655.957 FCFA, which holds for both the Central African (XAF) and the West African (XOF) franc. Every amount is per delivered message. | Market | Marketing | Utility | Authentication | Auth. international | Service (1) | | --------------------------------------------------------- | -------------------- | -------------------- | -------------------- | -------------------- | -------------------- | | Rest of Africa (Cameroon, Côte d'Ivoire, Senegal, Kenya…) | €0.0186 (12.20 FCFA) | €0.0033 (2.16 FCFA) | €0.0033 (2.16 FCFA) | — | €0.0033 (2.16 FCFA) | | Nigeria (+234) | €0.0428 (28.07 FCFA) | €0.0056 (3.67 FCFA) | €0.0056 (3.67 FCFA) | €0.0622 (40.80 FCFA) | €0.0056 (3.67 FCFA) | | Egypt (+20) | €0.0533 (34.96 FCFA) | €0.0030 (1.97 FCFA) | €0.0030 (1.97 FCFA) | €0.0538 (35.29 FCFA) | €0.0030 (1.97 FCFA) | | South Africa (+27) | €0.0314 (20.60 FCFA) | €0.0079 (5.18 FCFA) | €0.0079 (5.18 FCFA) | €0.0166 (10.89 FCFA) | €0.0079 (5.18 FCFA) | | Morocco (+212) | €0.0342 (22.43 FCFA) | €0.0190 (12.46 FCFA) | €0.0190 (12.46 FCFA) | €0.0669 (43.88 FCFA) | €0.0190 (12.46 FCFA) | | Other (DRC, Guinea…) (2) | €0.0500 (32.80 FCFA) | €0.0064 (4.20 FCFA) | €0.0064 (4.20 FCFA) | — | €0.0064 (4.20 FCFA) | If your WhatsApp Business Account is billed in dollars, this is Meta's USD rate card: | Market | Marketing | Utility | Authentication | Auth. international | Service (1) | | -------------- | --------- | ------- | -------------- | ------------------- | ----------- | | Rest of Africa | $0.0225 | $0.0040 | $0.0040 | — | $0.0040 | | Nigeria | $0.0516 | $0.0067 | $0.0067 | $0.0750 | $0.0067 | | Egypt | $0.0644 | $0.0036 | $0.0036 | $0.0650 | $0.0036 | | South Africa | $0.0379 | $0.0095 | $0.0095 | $0.0200 | $0.0095 | | Morocco | $0.0414 | $0.0230 | $0.0230 | $0.0811 | $0.0230 | | Other | $0.0604 | $0.0077 | $0.0077 | — | $0.0077 | (1) Beyond the 1,000 free service messages per number per month. (2) See the next section. Three things to know when reading these cards: * **Meta publishes a fixed card per currency** and bills each account in its own currency, at that card's rates, not at the day's exchange rate. Your account is billed in the currency chosen when it was created, and after that only Meta's currency migration API can change it. Coexistence accounts (a number kept on the WhatsApp Business app) are not eligible for this API, so pick the currency when the account is created. Only the euro card converts to CFA francs at a fixed rate; a dollar account depends on the rate your bank applies. * **The "authentication-international" rate only targets large senders based abroad.** Meta applies it only to businesses that send more than 750,000 messages outside the customer service window over a rolling 30 days to the markets concerned, and only to recipients in a country other than their own. A Cameroonian small business sending codes to Nigeria pays the regular authentication rate. * **Marketing weighs the most.** In "Rest of Africa", a marketing message costs 5.6 times a utility one. ## Which African countries are billed at the "Rest of Africa" rate? What counts is the calling code of the **recipient's** number, not the country your business is in. Meta groups 40 countries under "Rest of Africa": Algeria, Angola, Benin, Botswana, Burkina Faso, Burundi, Cameroon, Chad, Republic of the Congo (Brazzaville), Eritrea, Ethiopia, Gabon, Gambia, Ghana, Guinea-Bissau, Côte d'Ivoire, Kenya, Lesotho, Liberia, Libya, Madagascar, Malawi, Mali, Mauritania, Mozambique, Namibia, Niger, Rwanda, Senegal, Sierra Leone, Somalia, South Sudan, Sudan, Eswatini, Tanzania, Togo, Tunisia, Uganda, Zambia and Zimbabwe. * **Morocco left the group on October 1, 2026.** It now has its own, more expensive line and an authentication-international rate. * **Nigeria, Egypt and South Africa** each have their own rates. * **A country missing from Meta's list is billed at the "Other" rate.** That is the case for several countries you would expect in the African group: the Democratic Republic of the Congo (+243), Guinea (+224), the Central African Republic (+236), Equatorial Guinea (+240), Djibouti (+253) and Mauritius (+230). A marketing message there costs €0.05, or 32.80 FCFA: more than twice the "Rest of Africa" rate. A merchant in Douala messaging a +243 customer therefore pays the "Other" rate. ## Do WhatsApp rates go down with volume? Yes, but only for utility and authentication. Meta counts each month's charged messages per market and per category, adding up every WhatsApp account in your business portfolio. Marketing and service have no tiers, and free messages do not count. "Rest of Africa" tiers on the euro card: | Utility, messages per month | Authentication, messages per month | Rate | Discount shown by Meta | | --------------------------- | ---------------------------------- | ------- | ---------------------- | | 0 to 100,000 | 0 to 300,000 | €0.0033 | list rate | | 100,001 to 1,000,000 | 300,001 to 2,000,000 | €0.0031 | −5% | | 1,000,001 to 4,500,000 | 2,000,001 to 10,000,000 | €0.0030 | −10% | | 4,500,001 to 40,000,000 | 10,000,001 to 20,000,000 | €0.0028 | −15% | | 40,000,001 to 80,000,000 | 20,000,001 to 40,000,000 | €0.0026 | −20% | | Above 80,000,000 | Above 40,000,000 | €0.0025 | −25% | The lower rate applies only to the messages that fall inside that band, never retroactively to the whole month, and the count resets every month. Nigeria, Egypt, South Africa and Morocco use the same thresholds on their own rates. In practice, a business sending a few thousand notifications a month always pays the list rate. ## How much does a shop in Cameroon pay each month? Take an online shop in Douala: one WhatsApp number, customers in Cameroon (+237, so "Rest of Africa") and a WhatsApp Business Account billed in euros. Every month it sends 1,000 order notifications, 200 login codes and 400 free-form replies to customer questions. | Item | Volume | Meta rate | Monthly cost | | ---------------------------------------- | ------ | --------------------------- | ------------------------- | | Order notifications (utility) | 1,000 | €0.0033 | €3.30 ≈ 2,165 FCFA | | Login codes (authentication) | 200 | €0.0033 | €0.66 ≈ 433 FCFA | | Free-form replies (service) | 400 | free under 1,000 per number | 0 FCFA | | **Meta fees** | | | **€3.96 ≈ 2,598 FCFA** | | Genuka WA Growth plan, 1 number, monthly | | | 10,000 FCFA | | **Total** | | | **≈ 12,598 FCFA a month** | The 1,600 outbound messages exceed the Starter allowance (500 per number per month) and fit easily in Growth (5,000). Stay on monthly billing: on a yearly plan, the Growth allowance currently covers the whole year, and those 5,000 messages would run out in just over three months at this pace. On a WhatsApp account billed in dollars, the same Meta fees are 1,200 × $0.004 = $4.80. Add a monthly promotion to 2,000 customers as a marketing template: 2,000 × €0.0186 = €37.20, about 24,402 FCFA more on the Meta side. That one campaign costs almost ten times the rest of the traffic: marketing is what makes the bill. > [!WARNING] > **Two limits until the business is verified** > > Meta reserves authentication templates for businesses that have completed one of its scaling > paths, usually Meta business verification. Without it, creating an authentication template fails > with a message that wrongly blames the app: "Application does not have permission for this > action" (see error [141010](https://wa.genuka.com/en/docs/errors/141010)). And a new business portfolio can only reach > 250 distinct numbers per 24 hours outside the customer service window: the 2,000-customer > promotion would take more than a week to go out. Business verification unlocks authentication > templates, and it is one of the paths Meta offers to raise that limit. The details are in > [WhatsApp API without Meta verification](https://wa.genuka.com/en/docs/guides/without-meta-verification). ## Who bills what: Meta or Genuka WA? **Meta bills the messages**, directly to the customer's own WhatsApp Business Account and to the payment method the customer added with Meta. Genuka is a Meta Tech Provider, not a BSP: we do not resell messages, we add no markup to them, and there are no credits or wallet to top up. **Genuka WA charges a subscription per WhatsApp number.** The base price covers the first number, each extra number costs less, and the plan includes an allowance of outbound messages per number. That allowance caps your sends; it is never billed per message. | Tier | Monthly | Yearly (per month) | Extra number | Messages per number per period | Max numbers | | ---------- | ----------- | ------------------ | ------------ | ------------------------------ | ----------- | | Starter | 5,000 FCFA | 3,000 FCFA | 2,500 FCFA | 500 | 5 | | Growth | 10,000 FCFA | 8,000 FCFA | 2,200 FCFA | 5,000 | 15 | | Scale | 30,000 FCFA | 25,000 FCFA | 1,800 FCFA | 30,000 | 100 | | Enterprise | quote | quote | quote | custom | custom | The message allowance applies per billing period: one month on a monthly plan. On a yearly plan it currently covers the whole year and is not multiplied by twelve, so for a steady monthly volume, choose monthly billing. The same tiers exist in euros and dollars, on a price list of their own: €19, €49 and €149 (or $19, $49 and $149) a month, or 15, 39 and 119 a month paid yearly. Every account starts with a 7-day free trial of the Growth tier, no card required. The subscription is paid by mobile money (Genuka Pay in Cameroon, pawaPay in the other CFA franc countries we cover) or by card (Stripe). Details are on the [pricing grid](https://wa.genuka.com/en#pricing), in [Plans & billing](https://wa.genuka.com/en/docs/billing) and, for an AI agent, in [/pricing.md](https://wa.genuka.com/pricing.md). ## How do I check what Meta charges me? 1. ### Add a payment method with Meta In [Meta's Billing Hub](https://business.facebook.com/latest/billing_hub/payment_methods/), attach a card to the connected WhatsApp Business Account. Your Genuka WA subscription does not pay for messages. 2. ### Check the category of every approved template The category sets the price. List your templates with `GET /api/v1/templates?sync=true`, which re-reads the category Meta actually applied before answering. Without it, the list can still show the category requested at creation after Meta has changed it (see the [API reference](https://wa.genuka.com/en/docs/api) and [Templates & campaigns](https://wa.genuka.com/en/docs/campaigns)). 3. ### Read the `pricing` field on every status Every message status reaches your [webhooks](https://wa.genuka.com/en/docs/webhooks) with, inside `data`, the `pricing` object Meta attaches to it, untouched: ```json title="POST https://your-app.com/webhooks/whatsapp (excerpt)" { "type": "message_status", "field": "messages", "data": { "id": "wamid.HBgLMjM3...", "status": "delivered", "pricing": { "billable": true, "pricing_model": "PMP", "type": "regular", "category": "utility" } } } ``` `type: "regular"` means the message is charged. `free_customer_service` marks a free message, for instance a service message still under the 1,000 free tier; `free_entry_point`, a message sent inside a free entry point window. 4. ### Add it up with the API `GET /api/v1/analytics?kind=pricing` returns the volume and the cost Meta reports, broken down by category and country. With a partner key, `connectionId` is required; with a client key, it picks the number, and the main number is read by default. **curl** ```bash curl "https://wa.genuka.com/api/v1/analytics?kind=pricing&connectionId=con_1&start=2026-10-01&dimensions=PRICING_CATEGORY,COUNTRY" \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" ``` **Node.js** ```js // ES module (.mjs file or "type": "module"): this uses top-level await. const params = new URLSearchParams({ kind: "pricing", connectionId: "con_1", start: "2026-10-01", dimensions: "PRICING_CATEGORY,COUNTRY", }); const response = await fetch(`https://wa.genuka.com/api/v1/analytics?${params}`, { headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}` }, }); if (!response.ok) { throw new Error(`${response.status} ${await response.text()}`); } const body = await response.json(); for (const point of body.data.points) { console.log(point.day, point.dimensions, point.volume, point.cost); } ``` **Python** ```python import os import requests response = requests.get( "https://wa.genuka.com/api/v1/analytics", params={ "kind": "pricing", "connectionId": "con_1", "start": "2026-10-01", "dimensions": "PRICING_CATEGORY,COUNTRY", }, headers={"Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}"}, timeout=30, ) response.raise_for_status() for point in response.json()["data"]["points"]: print(point["day"], point["dimensions"], point.get("volume"), point["cost"]) ``` **PHP** ```php 'pricing', 'connectionId' => 'con_1', 'start' => '2026-10-01', 'dimensions' => 'PRICING_CATEGORY,COUNTRY', ]); $ch = curl_init("https://wa.genuka.com/api/v1/analytics?$query"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('GENUKA_WA_API_KEY')], ]); $raw = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($status !== 200) { exit("HTTP $status: $raw" . PHP_EOL); } $body = json_decode($raw, true); foreach ($body['data']['points'] as $point) { echo $point['day'], ' ', json_encode($point['dimensions']), ' ', json_encode($point['cost']), PHP_EOL; } ``` ```json title="200 OK (excerpt)" { "data": { "kind": "pricing", "source": "meta", "granularity": "DAILY", "points": [ { "start": 1790812800, "end": 1790899200, "day": "2026-10-01", "volume": 48, "cost": { "available": true, "amount": 0.1584 }, "dimensions": { "pricing_category": "UTILITY", "country": "CM" } } ], "days": [ { "day": "2026-10-01", "totals": { "volume": 48, "cost": { "available": true, "amount": 0.1584 } }, "points": ["…"] } ] }, "window": { "start": "2026-10-01T00:00:00.000Z", "end": "2026-10-08T09:00:00.000Z" }, "truncated": false, "archivedDays": 8, "costAvailable": true, "retentionDays": 365 } ``` `days` groups the same points per day, with a daily total in `totals`. When `costAvailable` is `false`, Meta reported no cost for the period: that is not a zero, do not add it up as one. The API only reads the last 365 days from Meta (`retentionDays`). Every read is archived on Genuka's side, and `source=archive` returns the days a previous read already archived, including those Meta no longer serves. A day nobody read before it fell out of those 365 days is not archived: run this request at least once a month to keep the full history. ## How do I lower my WhatsApp bill? * **Reply inside the 24-hour window.** Up to 1,000 free-form replies per number per month cost nothing. * **Target your campaigns.** Marketing costs 5.6 times utility in "Rest of Africa": 500 interested customers beat 5,000 random recipients. * **Bring customers in through Click to WhatsApp ads.** If you reply within 24 hours, the conversation that follows is free for as long as the free entry point window stays open. * **In the CFA zone, prefer a WhatsApp account billed in euros.** The fixed parity makes the cost in CFA francs predictable, bank fees aside; a dollar account follows the exchange rate. Decide when the account is created: a coexistence account cannot switch currency later. ## FAQ ### Are WhatsApp Business messages free in Africa? Not all of them. Messages your customers send you are free, and so are the first 1,000 free-form replies on each number every month. Templates — marketing, utility, authentication — are charged from the first delivered message, including inside the 24-hour window since October 1, 2026, except inside a free entry point window opened by a Click to WhatsApp ad. ### How much does a WhatsApp OTP cost in Cameroon? €0.0033 per delivered code on an account billed in euros, about 2.16 CFA francs, or $0.004 on a dollar account. The rate is the same in every "Rest of Africa" country. Creating an authentication template does, however, require the business to have completed one of Meta's scaling paths, usually business verification. ### Does Genuka WA take a commission on messages? No. Meta bills messages directly to your WhatsApp Business Account, at Meta's rates, with no markup from Genuka. Genuka WA only charges a subscription per number, from 5,000 FCFA (€19) a month, or 3,000 FCFA a month paid yearly. ### Why did my Meta bill go up in October 2026? Since October 1, 2026, Meta charges service messages beyond 1,000 per number per month, and utility templates sent inside the 24-hour window, which used to be free. Rates also went up in Morocco, which left the "Rest of Africa" group, and for utility and authentication in South Africa. ### Does the price depend on my country or my customer's? Your customer's: Meta applies the rate of their number's calling code. A business in Abidjan messaging a Nigerian customer (+234) pays the Nigeria rate. ## Sources * Meta, [Pricing on the WhatsApp Business Platform](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing), page updated September 30, 2026, and its USD and EUR rate cards "effective October 1, 2026" (download links in the *Rate cards and volume tiers* section). * Meta, [Upcoming pricing updates for Meta Business Agent, service and utility messages](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing/non-template-messages), page updated September 28, 2026. * Meta, [Authentication-international rates](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing/authentication-international-rates). * Meta, [Change billing currency via API](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing/change-billing-currency), page updated June 23, 2026 (coexistence accounts are not eligible). * Meta, [Template categorization](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/template-categorization), page updated September 15, 2026 (automatic category updates). * Meta, [Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits). * Meta, [Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) (code `131042`). * Meta, [Analytics](https://developers.facebook.com/documentation/business-messaging/whatsapp/analytics) (`pricing_analytics`). * BCEAO, [Histoire du franc CFA](https://www.bceao.int/fr/content/histoire-du-franc-cfa) (parity €1 = 655.957 FCFA). * French Treasury, [Les principes et modalités de fonctionnement de la coopération monétaire](https://www.tresor.economie.gouv.fr/tresor-international/la-zone-franc/les-principes-et-modalites-de-fonctionnement-de-la-cooperation-monetaire) (the same fixed parity for the West and Central African CFA francs, in French). * 360dialog, [Authentication messages](https://docs.360dialog.com/docs/resources/authentication-messages) (requirements for authentication templates). * Genuka WA, [Plans & billing](https://wa.genuka.com/en/docs/billing), [/pricing.md](https://wa.genuka.com/pricing.md), [error 141010](https://wa.genuka.com/en/docs/errors/141010) and [WhatsApp API without Meta verification](https://wa.genuka.com/en/docs/guides/without-meta-verification). --- # WhatsApp API for AI agents: MCP server, llms.txt URL: https://wa.genuka.com/en/docs/guides/ai-agents Language: English > Connect Claude, Cursor or VS Code to the WhatsApp Business API with the Genuka WA MCP server, plus llms.txt, Markdown docs and an OpenAPI spec. To let an AI agent work with WhatsApp through Genuka WA, add the `@genuka/whatsapp-mcp` MCP server to your client with an API key: Claude, Cursor or VS Code can then list your numbers, send templates and run campaigns. To have an assistant write the integration code instead, point it at `/llms.txt`, the `.mdx` pages and `/openapi.json`. *Last updated October 8, 2026* ## What can an AI agent do with Genuka WA? Genuka WA is a REST API for the official WhatsApp Business Platform (Meta's Cloud API). Genuka is a Meta Tech Provider: you connect your own WhatsApp Business number through Meta's Embedded Signup, then send notifications, one-time codes and campaigns over HTTP. An agent can use it in two ways, and most teams end up with both: | You want the agent to… | Use | What it gets | | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------- | | Act on your account: send a message, create a template, launch a campaign, check a number's health | The MCP server `@genuka/whatsapp-mcp` | 17 tools, each mapped to one `/api/v1` endpoint | | Write code in your repo that calls Genuka WA | `/llms.txt`, `.mdx` pages, `/openapi.json`, `/pricing.md` | The documentation and the API contract, in formats a model reads without a browser | [MCP](https://modelcontextprotocol.io/docs/getting-started/intro) is the open standard AI applications use to connect to external tools; Claude, ChatGPT, VS Code and Cursor all support it. ## How do I install the Genuka WA MCP server? 1. ### Create an API key In the dashboard, open the [API keys page](https://wa.genuka.com/dashboard/settings) and create a key (`pk_live_…`). It is shown once. If the agent only works for one of your businesses, pick that business when you create the key: the agent will not be able to see or touch the others (see [Authentication](https://wa.genuka.com/en/docs/authentication)). You need Node.js 20 or later: the client starts the server with `npx`. 2. ### Add the server to your client **Claude Code** ```bash claude mcp add --env GENUKA_WA_API_KEY=pk_live_xxx --transport stdio genuka-wa -- npx -y @genuka/whatsapp-mcp ``` To share it with your team without committing the key, put this `.mcp.json` at the root of the repo; Claude Code expands `${GENUKA_WA_API_KEY}` from each developer's environment: ```json title=".mcp.json" { "mcpServers": { "genuka-wa": { "command": "npx", "args": ["-y", "@genuka/whatsapp-mcp"], "env": { "GENUKA_WA_API_KEY": "${GENUKA_WA_API_KEY}" } } } } ``` **Claude Desktop** **Settings → Developer → Edit Config** opens `claude_desktop_config.json` (`~/Library/Application Support/Claude/` on macOS, `%APPDATA%\Claude\` on Windows). Add the server, save, then quit and restart Claude Desktop. ```json title="claude_desktop_config.json" { "mcpServers": { "genuka-wa": { "command": "npx", "args": ["-y", "@genuka/whatsapp-mcp"], "env": { "GENUKA_WA_API_KEY": "pk_live_xxx" } } } } ``` **Cursor** In `~/.cursor/mcp.json` (every project) or `.cursor/mcp.json` (this project). `${env:…}` reads the key from your environment: ```json title=".cursor/mcp.json" { "mcpServers": { "genuka-wa": { "command": "npx", "args": ["-y", "@genuka/whatsapp-mcp"], "env": { "GENUKA_WA_API_KEY": "${env:GENUKA_WA_API_KEY}" } } } } ``` **VS Code** In `.vscode/mcp.json`, or through **MCP: Open User Configuration** for every workspace. VS Code asks for the key once and does not store it in the file: ```json title=".vscode/mcp.json" { "inputs": [ { "type": "promptString", "id": "genuka-wa-api-key", "description": "Genuka WA API key", "password": true } ], "servers": { "genuka-wa": { "type": "stdio", "command": "npx", "args": ["-y", "@genuka/whatsapp-mcp"], "env": { "GENUKA_WA_API_KEY": "${input:genuka-wa-api-key}" } } } } ``` **Windsurf** Windsurf has been called Devin Desktop since June 2, 2026. In the Cascade panel, open the `…` menu, then **Open MCP config file**: ```json title="mcp_config.json" { "mcpServers": { "genuka-wa": { "command": "npx", "args": ["-y", "@genuka/whatsapp-mcp"], "env": { "GENUKA_WA_API_KEY": "pk_live_xxx" } } } } ``` `GENUKA_WA_BASE_URL` is optional and defaults to `https://wa.genuka.com`. 3. ### Check the connection In Claude Code, `claude mcp list` or `/mcp` shows the server as connected. Then ask the agent "List my WhatsApp numbers": it calls `list_numbers`, which returns each number's `id` (the `connectionId` every other tool needs), its business and its quality rating. If the key is missing or wrong, the tool answers with the reason instead of failing silently. ## Which tools does the MCP server expose? | Tool | What it does | Sends or changes something? | | -------------------------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------- | | `list_numbers` | Connected numbers and their health | No | | `get_number_health` | Live quality rating, messaging limit tier, throughput, alerts | No | | `send_text_message` | Free-form text, inside the 24-hour window | Sends a message | | `send_template_message` | An approved template: variables, header, buttons, one-time code | Sends a message | | `send_media_message` | Image, video, audio, document or sticker | Sends a message | | `list_templates` / `get_template` | Templates, their status and Meta's review outcome | No | | `create_template` | Submits a template to Meta for review | Yes | | `list_webhooks` / `create_webhook` | Webhook endpoints; creation returns the signing secret once | Creation sends your account's events, customer messages included, to the URL | | `test_webhook` | Signed sample events sent to your endpoint | Calls your endpoint | | `list_campaigns` / `get_campaign` / `list_campaign_recipients` | Campaigns and per-recipient delivery | No | | `create_campaign` | A draft: one template, one number, a recipient list | Yes, sends nothing | | `launch_campaign` | Sends the campaign to every pending recipient | Sends messages | | `get_subscription` | Plan, usage and limits (account-wide key only) | No | Each tool declares the MCP hints `readOnlyHint`, `destructiveHint` and `idempotentHint`, so your client can approve reads automatically and ask you before anything that sends. Errors come back to the agent with the API's own code (`plan_limit_messages`, `connection_not_found`, `meta_rejected`…), Meta's code and trace id when there is one, and what to do next. The server reads; it does not receive. Customer replies and delivery statuses arrive on your webhooks — see [Receive WhatsApp replies and statuses via webhook](https://wa.genuka.com/en/docs/guides/receive-messages-webhooks). ## What should I check before letting an agent send messages? * **It is a real message, to a real person.** A send from the agent leaves from your number, like one from your backend. Keep your client's confirmation prompt on for the tools that send, and ask for the campaign to be shown to you before `launch_campaign`. * **A webhook is a data exit.** `create_webhook` forwards every event it subscribes to, including your customers' phone numbers and messages, to its URL for as long as the endpoint stays active. Keep the confirmation prompt on for it too, and check that the URL is one you gave the agent, not one it read in a web page or a file. The server tells the agent the same. * **The 24-hour window.** When a customer messages you, a 24-hour customer service window opens; once it closes, only pre-approved templates can be sent ([Meta](https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/send-messages)). When Genuka WA cannot confirm that the window is open, the send result carries a `warning`, and the server puts it at the top of what the agent reads, so it reaches you. * **What it costs.** Each message counts against your plan's allowance for the billing period ([plans](https://wa.genuka.com/en#pricing), [`/pricing.md`](https://wa.genuka.com/pricing.md)). Meta charges for a template message when it is delivered, and non-template messages are free ([Meta](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing)); Meta bills your own WhatsApp Business Account, with no markup from Genuka. * **The key.** Keep it out of git with the `${…}` variables shown above, give the agent a key restricted to one business when that is enough, and revoke it from the dashboard when you are done. ## How do I give my coding assistant the Genuka WA docs? When the agent's job is to write your integration, it needs the contract more than the tools. Everything below is public, needs no API key, and stays in sync with the published docs: | URL | Contents | When to use it | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | [`/llms.txt`](https://wa.genuka.com/llms.txt) | Index of every page (English, French, SDK) with one line each, following the [llms.txt](https://llmstxt.org/) proposal | The first file to hand an agent | | [`/llms-full.txt`](https://wa.genuka.com/llms-full.txt) | Every page's Markdown in a single file | Load the whole documentation into context at once | | Any page + `.mdx` | One page in Markdown, e.g. [`/en/docs/messages.mdx`](https://wa.genuka.com/en/docs/messages.mdx) | Point to exactly the page that matters | | [`/openapi.json`](https://wa.genuka.com/openapi.json) | OpenAPI 3.1 description of `/api/v1` | Generate a typed client, or let the agent check field names | | [`/pricing.md`](https://wa.genuka.com/pricing.md) | Plans, prices and limits in Markdown, generated from the billing catalog | Answer "how much will this cost me?" | ```bash curl https://wa.genuka.com/llms.txt curl https://wa.genuka.com/en/docs/webhooks.mdx ``` ## What prompt should I give the assistant? Name the product, the framework and the event, and let it read the docs. For example: > Add WhatsApp order-shipped notifications to my Next.js app with Genuka WA. Read > [https://wa.genuka.com/llms.txt](https://wa.genuka.com/llms.txt) first. A good result has three parts: a `UTILITY` template such as `order_shipped`, submitted once and approved by Meta; server-side code that calls `POST /api/v1/messages` when the order ships; and a webhook route that verifies `X-Genuka-Signature` before trusting a status. With the `order_shipped` template of the [order notifications guide](https://wa.genuka.com/en/docs/guides/order-notifications) (three body variables and a tracking-link button), the call looks like this. The values must match the template's variables one for one, or Meta refuses the message ([error 132000](https://wa.genuka.com/en/docs/errors/132000)): **curl** ```bash curl -X POST https://wa.genuka.com/api/v1/messages \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "to": "+237690000001", "template": { "name": "order_shipped", "language": "en", "body": ["Awa", "ORD-1042", "DHL"], "buttons": [{ "type": "url", "text": "ORD-1042" }] } }' ``` **Node.js** ```ts title="lib/whatsapp.ts" export async function notifyOrderShipped(phone: string, firstName: string, orderNumber: string, carrier: string) { const response = await fetch("https://wa.genuka.com/api/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ connectionId: process.env.GENUKA_WA_CONNECTION_ID, to: phone, // E.164: "+237690000001" template: { name: "order_shipped", language: "en", body: [firstName, orderNumber, carrier], // {{1}}, {{2}}, {{3}} in order buttons: [{ type: "url", text: orderNumber }], // completes the tracking link }, }), }); const result = await response.json(); if (!response.ok) throw new Error(`${result.error}: ${result.message ?? ""}`); return result.data.messageId as string; // accepted by Meta, not yet delivered } ``` **Python** ```python title="whatsapp.py" import os import requests def notify_order_shipped(phone: str, first_name: str, order_number: str, carrier: str) -> str: response = requests.post( "https://wa.genuka.com/api/v1/messages", headers={"Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}"}, json={ "connectionId": os.environ["GENUKA_WA_CONNECTION_ID"], "to": phone, # E.164: "+237690000001" "template": { "name": "order_shipped", "language": "en", "body": [first_name, order_number, carrier], # {{1}}, {{2}}, {{3}} in order "buttons": [{"type": "url", "text": order_number}], # completes the tracking link }, }, timeout=30, ) result = response.json() if not response.ok: raise RuntimeError(f"{result['error']}: {result.get('message', '')}") return result["data"]["messageId"] # accepted by Meta, not yet delivered ``` **PHP** ```php title="whatsapp.php" true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('GENUKA_WA_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'connectionId' => getenv('GENUKA_WA_CONNECTION_ID'), 'to' => $phone, // E.164: "+237690000001" 'template' => [ 'name' => 'order_shipped', 'language' => 'en', 'body' => [$firstName, $orderNumber, $carrier], // {{1}}, {{2}}, {{3}} in order 'buttons' => [['type' => 'url', 'text' => $orderNumber]], // completes the tracking link ], ]), ]); $result = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); if ($status >= 400) { throw new RuntimeException($result['error'] . ': ' . ($result['message'] ?? '')); } return $result['data']['messageId']; // accepted by Meta, not yet delivered } ``` The full walkthrough (template, idempotency, delivery tracking) is in [WhatsApp order notifications from your backend](https://wa.genuka.com/en/docs/guides/order-notifications). ## What should I put in AGENTS.md? `AGENTS.md`, `CLAUDE.md` or `.cursor/rules` are read by the assistant before every task. These lines spare it the mistakes we see most often in WhatsApp integrations: ```md title="AGENTS.md" ## WhatsApp (Genuka WA) - WhatsApp goes through the Genuka WA REST API: base URL https://wa.genuka.com/api/v1, header `Authorization: Bearer $GENUKA_WA_API_KEY`. Server-side only: never ship the key to a browser or a mobile app. We do not call Meta's Graph API directly. - Docs for agents: https://wa.genuka.com/llms.txt; append `.mdx` to any docs URL for Markdown; API contract: https://wa.genuka.com/openapi.json. - `connectionId` is the `id` returned by `GET /api/v1/connections`. It lives in config (`GENUKA_WA_CONNECTION_ID`), never hard-coded. - Phone numbers are E.164: `+237690000001`. - Business-initiated messages (order updates, reminders, codes) are templates. Free-form `text` is only delivered within 24 hours of the customer's last message. - Templates are created once (`POST /api/v1/templates`) and reviewed by Meta; only `approved` ones can be sent. Use the template's exact `language`. - A 200 from `POST /api/v1/messages` means Meta accepted the message, not that it was delivered. Delivery and replies arrive on webhooks; verify `X-Genuka-Signature` on the raw body first. - Errors are `{ "error": "code", "message": "…" }`. Never retry `plan_limit_messages`; when the body has `meta.retryable: false`, change the request instead of retrying it. ``` ## FAQ ### Does the MCP server send real WhatsApp messages? Yes. `send_text_message`, `send_template_message`, `send_media_message` and `launch_campaign` send from your connected number to real recipients, and count against your plan's allowance. Reads (`list_*`, `get_*`) change nothing. ### Is the MCP server free? The `@genuka/whatsapp-mcp` package is MIT-licensed and free. It uses your Genuka WA account, billed as a subscription per WhatsApp number ([plans](https://wa.genuka.com/en#pricing)). Meta bills delivered template messages to your own WhatsApp Business Account, with no markup from Genuka. ### Which AI clients does it work with? Any MCP client that can start a local (stdio) server: Claude Code, Claude Desktop, Cursor, VS Code and Windsurf are covered above. The server needs Node.js 20 or later. ### Can the agent read my customers' replies? Not through the MCP server: it has no tool for incoming messages. Replies and delivery statuses are pushed to your webhook endpoint, which the agent can create with `create_webhook` (at a URL you give it) and check with `test_webhook`. ### How do I limit what the agent can reach? Give it a key restricted to one business. Lists then return only that business's rows, any other id answers `404`, and `get_subscription` is refused — see [Authentication](https://wa.genuka.com/en/docs/authentication). ## Sources * [Meta — Send messages (customer service window)](https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/send-messages) * [Meta — Pricing on the WhatsApp Business Platform](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing) * [Model Context Protocol — What is MCP?](https://modelcontextprotocol.io/docs/getting-started/intro) * [The /llms.txt proposal](https://llmstxt.org/) * [Claude Code — Connect Claude Code to tools via MCP](https://code.claude.com/docs/en/mcp) * [MCP — Connect to local MCP servers (Claude Desktop)](https://modelcontextprotocol.io/docs/develop/connect-local-servers) * [Cursor — Model Context Protocol](https://cursor.com/docs/context/mcp) * [VS Code — MCP configuration reference](https://code.visualstudio.com/docs/copilot/reference/mcp-configuration) * [Devin Desktop (formerly Windsurf) — MCP](https://docs.devin.ai/desktop/cascade/mcp) * [Devin Desktop FAQ — Windsurf becomes Devin Desktop](https://docs.devin.ai/desktop/devin-desktop-faq) --- # API reference URL: https://wa.genuka.com/en/docs/api Language: English > One API to drive everything programmatically: numbers, templates, messages, campaigns and your subscription. Build your own product on top of it. > [!NOTE] > **OpenAPI specification** > > The whole API is described in OpenAPI 3.1 at > [`https://wa.genuka.com/openapi.json`](https://wa.genuka.com/openapi.json): import it into > Postman or Insomnia, or give it to an AI coding agent. For agents, > [`/llms.txt`](https://wa.genuka.com/llms.txt) indexes the whole documentation. ## Authentication All requests use a server-side API key. Generate one in your dashboard under **API keys** — it looks like `pk_live_…` and is shown only once. Send it as a bearer token: ```http title="Header" Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxx ``` > [!WARNING] > **Keep keys server-side** > > A key can send messages from every number it covers. Never expose it in a browser or mobile app — > call the API from your backend only. ### Key scope A key is either account-wide or tied to a single connected business. Pick the business when generating the key in **API keys**. A scoped key resolves only that business: lists return its rows only, ids belonging to another one answer `404`, and `GET /api/v1/subscription` answers 403 — so you can hand it to that business (or run their integration) without exposing the rest of your account. Account-wide keys reach every number you have connected. ## Conventions Base URL `https://wa.genuka.com/api/v1`. Bodies and responses are JSON. List endpoints return `{ "data": [...] }`. Everything is automatically scoped to your account. Errors use HTTP status codes with `{ "error": "code", "message": "…" }`. Call `wa.genuka.com` over **https** and without a trailing slash: `http://` returns a `301` to https, `/api/v1/messages/` a `308` to the slashless form, and some HTTP clients drop the `Authorization` header when following a redirect. No other hostname serves this API. > [!WARNING] > **Send a User-Agent** > > Our CDN refuses a few known bot signatures before they reach the API. The one our integrators > hit most is `Python-urllib/3.x`, the header `urllib` sends by default: it answers `403` with the > body `error code: 1010`, which does not come from us and therefore carries no JSON. Send a > `User-Agent` that identifies your integration — which is what will let you find yourself in your > own logs anyway: > > ```python > request.add_header("User-Agent", "acme-crm/1.4 (+https://example.com)") > ``` > > `requests`, `curl`, `axios`, `node-fetch`, and the Go and Java clients pass with no change. ## Companies (connected businesses) | Method | Endpoint | Description | | ------ | ------------------------ | ------------------------------------------------ | | `GET` | `/api/v1/companies` | List connected businesses with their connections | | `GET` | `/api/v1/companies/{id}` | A single business + connections + counts | ```bash title="GET /api/v1/companies" curl https://wa.genuka.com/api/v1/companies -H "Authorization: Bearer pk_live_xxx" { "data": [ { "id": "cmp_123", "name": "Acme Coffee", "onboardedAt": "2026-06-01T10:12:00.000Z", "connections": [ { "id": "con_1", "wabaId": "1029…", "phoneNumberId": "1065…", "displayPhoneNumber": "+237 6 90 …", "qualityRating": "GREEN", "status": "connected" } ], "_count": { "templates": 4 } } ] } ``` ## Connections (numbers) | Method | Endpoint | Description | | ------ | --------------------- | ------------------------------------------------------------------- | | `GET` | `/api/v1/connections` | List WhatsApp connections (optional `?companyId=`, `?externalRef=`) | A connection is a WABA + phone number. Its `id` is what you pass when creating templates, campaigns or sending messages. ## Templates Templates are pre-approved message layouts. You need one to *start* a conversation (i.e. message a customer outside the 24-hour window — see [Messages](#messages)). You create a template here, Meta reviews it, and the approval / rejection lands automatically on `GET /templates/{id}` (and on your [webhooks](https://wa.genuka.com/en/docs/webhooks)). | Method | Endpoint | Description | | -------- | ------------------------ | --------------------------------------- | | `GET` | `/api/v1/templates` | List templates (optional `?companyId=`) | | `POST` | `/api/v1/templates` | Create & submit a template to Meta | | `GET` | `/api/v1/templates/{id}` | A template + its status history | | `POST` | `/api/v1/templates/sync` | Re-read every status from Meta | | `DELETE` | `/api/v1/templates/{id}` | Delete on Meta and locally | ### How creation works You pass Meta's `components` array **verbatim**. That keeps this endpoint thin while letting you build *any* template Meta supports — text, media headers, buttons, OTP, etc. The only required fields are `connectionId`, `name` and `components`. `category` is one of `MARKETING`, `UTILITY` or `AUTHENTICATION` (default `MARKETING`); `language` is a Meta locale such as `en_US` or `fr` (default `en`). ### Component reference A template is an ordered list of components. Each has a `type`: | Type | Shape | Description | | --------- | -------------------------------------------------------- | ----------------------------------------------------------------------------- | | `HEADER` | `format: TEXT \| IMAGE \| VIDEO \| DOCUMENT \| LOCATION` | Optional. One per template. Media headers need an example handle. | | `BODY` | `text` + `example` | Required (except AUTHENTICATION). Holds the `{{1}}`… or `{{name}}` variables. | | `FOOTER` | `text` | Optional short footer. No variables. | | `BUTTONS` | `QUICK_REPLY \| URL \| PHONE_NUMBER \| COPY_CODE \| OTP` | Optional. Up to 10 buttons (rules vary by type). | > [!NOTE] > **Examples are mandatory** > > Any component with variables (or a media header) must include an `example` so Meta can review it: > `"example": { "body_text": [["Alice", "#1024"]] }` for the body, > `"example": { "header_handle": [""] }` for a media header. ### Utility / marketing template (header + body + buttons) ```bash title="POST /api/v1/templates" curl -X POST https://wa.genuka.com/api/v1/templates \ -H "Authorization: Bearer pk_live_xxx" -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "name": "order_shipped", "language": "en_US", "category": "UTILITY", "components": [ { "type": "HEADER", "format": "IMAGE", "example": { "header_handle": ["4::aW1hZ2Uv..."] } }, { "type": "BODY", "text": "Hi {{1}}, order {{2}} just shipped. Track it any time.", "example": { "body_text": [["Alice", "#1024"]] } }, { "type": "FOOTER", "text": "Reply STOP to opt out" }, { "type": "BUTTONS", "buttons": [ { "type": "URL", "text": "Track order", "url": "https://example.com/track/{{1}}", "example": ["https://example.com/track/1024"] }, { "type": "QUICK_REPLY", "text": "Need help" } ] } ] }' { "data": { "id": "tpl_123", "status": "pending", "providerId": "12534…" } } ``` The media header `example.header_handle` is the resumable-upload handle returned by Meta's media upload — at *send* time you supply the real image by URL or media id (see [Messages → template](#messages)). ### Named parameters Prefer `{{name}}` over positional `{{1}}`? Set `parameterFormat: "NAMED"` and give each variable a `parameter_name` in the example. ```json title="POST /api/v1/templates (named)" { "connectionId": "con_1", "name": "appointment_reminder", "language": "en_US", "category": "UTILITY", "parameterFormat": "NAMED", "components": [ { "type": "BODY", "text": "Hi {{customer_name}}, your appointment is on {{date}}.", "example": { "body_text_named_params": [ { "param_name": "customer_name", "example": "Alice" }, { "param_name": "date", "example": "June 20" } ] } } ] } ``` ### Authentication template (OTP) Authentication templates deliver one-time codes. The body and button text are **fixed by WhatsApp** — you don't write copy. You only choose the button type and a few options. No header, media, URLs or emojis are allowed. | Type | Shape | Description | | ----------- | --------------------- | --------------------------------------------------------------- | | `COPY_CODE` | `otp_type: COPY_CODE` | Customer taps to copy the code. Simplest, works everywhere. | | `ONE_TAP` | `otp_type: ONE_TAP` | Android auto-fill. Needs `package_name` + `signature_hash`. | | `ZERO_TAP` | `otp_type: ZERO_TAP` | Code delivered silently to the app. Needs the same app binding. | ```json title="POST /api/v1/templates (authentication)" { "connectionId": "con_1", "name": "verification_code", "language": "en_US", "category": "AUTHENTICATION", "messageSendTtlSeconds": 600, "components": [ { "type": "BODY", "add_security_recommendation": true }, { "type": "FOOTER", "code_expiration_minutes": 10 }, { "type": "BUTTONS", "buttons": [ { "type": "OTP", "otp_type": "COPY_CODE", "text": "Copy code" } ] } ] } ``` For `ONE_TAP` / `ZERO_TAP`, add the app binding to the OTP button: `"autofill_text": "Autofill"`, `"supported_apps": [{ "package_name": "com.example.app", "signature_hash": "K8a..." }]`. > [!NOTE] > **Sending the code** > > Creating the template never sends anything. To deliver a code you send a `template` message with > the `otp` shorthand — see [Messages → authentication template](#messages). ### Refreshing statuses A review outcome reaches you as a [webhook](https://wa.genuka.com/en/docs/webhooks) — that is the path you should build on. But a webhook that was never delivered (an endpoint that was down, an integration added after the fact) leaves the template reading `pending` here long after Meta approved it, and a campaign refusing to launch with no visible reason. `POST /api/v1/templates/sync` re-reads the truth from Meta and writes it back. ```bash title="POST /api/v1/templates/sync" curl -X POST https://wa.genuka.com/api/v1/templates/sync \ -H "Authorization: Bearer pk_live_xxx" -H "Content-Type: application/json" \ -d '{ "companyId": "cmp_1" }' ``` The body is optional: with no field at all the refresh covers every WhatsApp account your key can reach, `companyId` narrows it to one client, and `connectionId` to a single number's WABA. The response names each WhatsApp account it touched — so one unreachable account is reported instead of hidden — and returns the freshly written rows, which saves a follow-up `GET`. ```json title="200 OK" { "data": { "wabas": [{ "wabaId": "1029…", "companyId": "cmp_1", "ok": true }], "synced": 1, "failed": 0, "templates": [ { "id": "tpl_1", "name": "order_shipped", "language": "fr", "status": "approved", "…": "…" } ] } } ``` A refresh where *every* account refused answers `502 meta_rejected` with the same `results` in the body; a partial failure stays a `200` you are expected to read. `GET /api/v1/templates?sync=true` does the same refresh inline before listing, and accepts the same `companyId` / `connectionId` filters. > [!NOTE] > **It is a repair path, not a polling loop** > > Each call spends one Meta read per WhatsApp account. Run it on a schedule if you like — hourly is > plenty — but keep the webhook as the way statuses normally arrive. ## Campaigns | Method | Endpoint | Description | | ------ | ----------------------------------- | -------------------------------------------- | | `GET` | `/api/v1/campaigns` | List campaigns (optional `?companyId=`) | | `POST` | `/api/v1/campaigns` | Create a campaign with recipients | | `GET` | `/api/v1/campaigns/{id}` | A campaign with delivery stats | | `GET` | `/api/v1/campaigns/{id}/recipients` | Per-recipient status (`?status=`, `?limit=`) | | `POST` | `/api/v1/campaigns/{id}/launch` | Send to all pending recipients | ### Create a campaign Each recipient carries its own `variables`. The simple form is a positional array for the body parameters `{{1}}, {{2}}…`. For media headers, buttons or OTP codes, pass a rich object instead — `{ "body": [...], "header": {...}, "buttons": [...] }` — the same shape accepted by [Messages → template](#messages). The template must be `approved` before launching. Below, `tpl_124` is a body-only template; `tpl_123` (`order_shipped` above, image header and URL button) would need the rich form for every recipient. ```bash title="POST /api/v1/campaigns" curl -X POST https://wa.genuka.com/api/v1/campaigns \ -H "Authorization: Bearer pk_live_xxx" -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "templateId": "tpl_124", "name": "June promo", "recipients": [ { "to": "+237690000001", "variables": ["Alice", "#1024"] }, { "to": "+237690000002", "variables": ["Bob", "#1025"] } ] }' { "data": { "id": "cmp_9", "name": "June promo", "status": "draft", "_count": { "recipients": 2 } } } ``` ### Launch ```bash title="POST /api/v1/campaigns/{id}/launch" curl -X POST https://wa.genuka.com/api/v1/campaigns/cmp_9/launch \ -H "Authorization: Bearer pk_live_xxx" { "data": { "sent": 2, "failed": 0, "skipped": 0 } } ``` > [!NOTE] > **Limits** > > We never charge for a message — Meta bills your WABA directly. Recipients do count against your > plan's message allowance for the period, and the whole list is checked before the first send, so a > launch that wouldn't fit is refused up front with `402 plan_limit_messages` rather than stopping > half-way. Only the recipients Meta actually accepted are consumed. V1 sends synchronously — keep > lists modest; queued sending for large lists is on the roadmap. ## Media Upload a file once, send it as many times as you need. Storage is **private**: nothing put here is reachable from a public URL, and Meta never fetches the file — we hand it the bytes server to server at send time. ```bash title="POST /api/v1/media (multipart)" curl -X POST https://wa.genuka.com/api/v1/media \ -H "Authorization: Bearer pk_live_xxx" \ -F "companyId=cmp_1" \ -F "file=@briefing.pdf" { "data": { "id": "ast_1", "url": "https://wa.genuka.com/api/v1/media/ast_1/content", "expiresAt": "2026-09-18T09:12:00.000Z", "mimeType": "application/pdf", "kind": "document", "sizeBytes": 481203, "filename": "briefing.pdf" }, "deduplicated": false } ``` > [!NOTE] > **Two limits, stated up front** > > **Retention — 30 days.** Every file is deleted permanently 30 days after upload, and > `expiresAt` says exactly when. That is Meta's own window: past it, their copy is gone anyway. A > send referencing an expired file answers `404 media_not_found`. > > **Storage — per plan.** 1 GB on Starter, 3 GB on Growth, 5 GB on Scale, custom on Enterprise, > across every client. The cap is on what is *held*: deleting a file gives the room back > immediately. An upload that would exceed it answers `402 plan_limit_media` before a single byte > is stored. `GET /api/v1/media` returns the current figure under `storage`. `url` is an endpoint on this API, not a share link: it requires your key and refuses any call outside its scope. Identical bytes already stored for this client come back as-is with a `200` and `deduplicated: true`, rather than being stored twice. | Route | Description | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `POST /api/v1/media` | Upload. Multipart: `file` (required), `companyId` (required with a partner-wide key), `filename` (optional). | | `GET /api/v1/media` | List. Filters `kind`, `companyId`, cursor pagination, plus the `storage` figure. | | `GET /api/v1/media/{id}` | One file's metadata. | | `GET /api/v1/media/{id}/content` | The bytes, authenticated by your key. | | `DELETE /api/v1/media/{id}` | Delete now, without waiting for expiry. | | `POST /api/v1/media/header-handle` | Produce the `header_handle` for a template header. Multipart: `file` (required), `connectionId` **or** `companyId` (neither, with a client key). | ### A template's media header: the `header_handle` Creating a template whose header is an IMAGE, VIDEO or DOCUMENT requires an `example.header_handle`, which Meta only issues through its Resumable Upload API. This is **not** the same thing as an `assetId`: a handle is consumed once, at template creation, and a send rejects it. Conversely an `assetId` is worthless at creation time. ```bash title="POST /api/v1/media/header-handle (multipart)" curl -X POST https://wa.genuka.com/api/v1/media/header-handle \ -H "Authorization: Bearer pk_live_xxx" \ -F "connectionId=con_1" \ -F "file=@catalogue.pdf" { "data": { "handle": "4::YXBwbGljYXRpb24vcGRm…", "filename": "catalogue.pdf", "mimeType": "application/pdf", "sizeBytes": 481203 } } ``` Name the client the way every other `/media` route expects (`-F "companyId=cmp_1"`) or one specific number (`-F "connectionId=con_1"`); a client key needs neither. The handle is the same either way — the upload session belongs to the Meta app, not to a number. Carry that `handle` into the template definition: ```json title="POST /api/v1/templates" { "connectionId": "con_1", "name": "morning_brief", "language": "en_US", "category": "UTILITY", "components": [ { "type": "HEADER", "format": "DOCUMENT", "example": { "header_handle": ["4::YXBwbGljYXRpb24vcGRm…"] } }, { "type": "BODY", "text": "Hi {{1}}, your brief for {{2}} is attached.", "example": { "body_text": [["Ana", "March 12"]] } } ] } ``` Accepted formats: `image/jpeg`, `image/png`, `video/mp4`, `video/3gpp`, `application/pdf`. Maximum size 4 MB — above that the response is `413 media_too_large`. Nothing is kept on our side: the handle is single-use, so archiving it would serve no purpose. ### Sending a stored file Pass `assetId` anywhere a media object is expected, template headers included. We resolve the Meta `media_id` for the right phone number and refresh it as it ages. ```json title="POST /api/v1/messages" { "connectionId": "con_1", "to": "+237600000000", "document": { "assetId": "ast_1", "filename": "briefing.pdf" } } ``` `id` (a Meta `media_id` you minted yourself) and `link` (a public URL Meta fetches) are still accepted. `assetId` is the one to prefer: it is the only one that exposes the file on no URL at all, and the only one that does not expire under you after 30 days. ## Messages | Method | Endpoint | Description | | ------ | ------------------ | --------------------------------- | | `POST` | `/api/v1/messages` | Send a single message of any type | One endpoint sends every WhatsApp message type. Always pass `connectionId` and `to`, then **exactly one** content field from the table below. Add `replyTo` (a received message's `wamid`) to quote/reply to a message. | Field | Window | Description | | ---------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- | | `template` | billable | Pre-approved template. The only way to start a conversation outside the 24h window. | | `text` | free\* | `{ "text": "Hi" }` or `{ "text": { "body": "…", "previewUrl": true } }` | | `image` / `video` / `audio` / `document` / `sticker` | free\* | `{ "image": { "assetId": "ast_1" } }` (recommended), `{ "link": "…" }` or `{ "id": "" }`; `caption`/`filename` optional | | `location` | free\* | `{ "location": { "latitude": …, "longitude": …, "name": "…", "address": "…" } }` | | `contacts` | free\* | `{ "contacts": [ … Meta contact objects … ] }` | | `reaction` | free\* | `{ "reaction": { "messageId": "wamid…", "emoji": "👍" } }` | | `buttons` / `list` / `cta` | free\* | Interactive reply buttons, a list menu, or a call-to-action URL button. | | `locationRequest` | free\* | `{ "locationRequest": { "body": "Where should we deliver?" } }` — shows a Send-location button. | | `raw` | free\* | Escape hatch for anything else (Flows, address, catalog, voice-call): a full Cloud API type fragment. | > [!NOTE] > **The 24-hour window** > > Only `template` sends can start a conversation. Everything marked `free*` is a free-form *session* > message: it only delivers if the customer messaged the business within the last 24 hours. Outside > that window, use a `template`. All sends return `{ "data": { "messageId": "wamid…" } }`. A `200` means Meta *accepted* the message, not that it was delivered. The final status (`sent` → `delivered` → `read`, or `failed`) arrives asynchronously on your [webhooks](https://wa.genuka.com/en/docs/webhooks). For `to`, always include the `+` and country code (e.g. `+237690000001`) — omitting it can misroute the message. Media passed by `link` is cached by Meta for \~10 minutes, so reuse the same URL for the same asset (or add a unique query string to bust the cache). ### Template message The simple case is a template whose only variables sit in its body, like `order_update` below (no media header, no dynamic button). `variables` is a positional array; you can also use `bodyNamed` for named templates, `header` for a media/text header, and `buttons` for dynamic button parameters. A send must fill every parameter the template declares: `order_shipped` above has an image header and a dynamic URL button, so it needs the second form — with body variables alone, Meta refuses it. ```bash title="POST /api/v1/messages (template, simple)" curl -X POST https://wa.genuka.com/api/v1/messages \ -H "Authorization: Bearer pk_live_xxx" -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "to": "+237690000001", "template": { "name": "order_update", "language": "en_US", "variables": ["Alice", "#1024"] } }' { "data": { "messageId": "wamid.HBg…" } } ``` ```json title="POST /api/v1/messages (template, header + body + button)" { "connectionId": "con_1", "to": "+237690000001", "template": { "name": "order_shipped", "language": "en_US", "header": { "image": { "link": "https://example.com/orders/1024.png" } }, "variables": ["Alice", "#1024"], "buttons": [ { "type": "url", "text": "1024" } ] } } ``` The `buttons[].text` fills the dynamic part of a URL button (the `{{1}}` in `https://example.com/track/{{1}}`). For a quick-reply use `{ "type": "quick_reply", "payload": "…" }`; for a coupon code use `{ "type": "copy_code", "code": "SAVE20" }`. Named body params: `"bodyNamed": { "customer_name": "Alice" }`. ### Authentication template Pass the code once via the `otp` shorthand — we fill both the body and the OTP button for you (the format Meta requires). ```json title="POST /api/v1/messages (authentication)" { "connectionId": "con_1", "to": "+237690000001", "template": { "name": "verification_code", "language": "en_US", "otp": "472913" } } ``` ### Free-form session messages ```json title="text" { "connectionId": "con_1", "to": "+237690000001", "text": "Thanks, talk soon!" } ``` ```json title="image (with caption)" { "connectionId": "con_1", "to": "+237690000001", "image": { "link": "https://example.com/promo.jpg", "caption": "New arrivals 🎉" } } ``` ```json title="document" { "connectionId": "con_1", "to": "+237690000001", "document": { "link": "https://example.com/invoice.pdf", "filename": "invoice-1024.pdf" } } ``` ```json title="interactive reply buttons" { "connectionId": "con_1", "to": "+237690000001", "buttons": { "body": "Confirm your order?", "footer": "Acme Coffee", "buttons": [ { "id": "yes", "title": "Confirm" }, { "id": "no", "title": "Cancel" } ] } } ``` ```json title="interactive list" { "connectionId": "con_1", "to": "+237690000001", "list": { "body": "Pick a delivery slot", "button": "Choose", "sections": [ { "title": "Today", "rows": [ { "id": "t1", "title": "12:00–14:00" }, { "id": "t2", "title": "14:00–16:00", "description": "Most popular" } ] } ] } } ``` ```json title="call-to-action URL button" { "connectionId": "con_1", "to": "+237690000001", "cta": { "body": "Your receipt is ready.", "displayText": "View receipt", "url": "https://example.com/r/1024" } } ``` ```json title="reply in-thread + reaction" { "connectionId": "con_1", "to": "+237690000001", "replyTo": "wamid.HBg…", "text": "On its way!" } { "connectionId": "con_1", "to": "+237690000001", "reaction": { "messageId": "wamid.HBg…", "emoji": "👍" } } ``` ## Subscription | Method | Endpoint | Description | | ------ | ---------------------- | ------------------------------ | | `GET` | `/api/v1/subscription` | Current plan, limits and usage | ```json title="GET /api/v1/subscription" { "plan": { "code": "growth", "name": "Growth" }, "interval": "monthly", "currency": "XAF", "status": "active", "currentPeriodEnd": "2026-07-18T00:00:00.000Z", "billedNumbers": 9, "usage": { "numbers": 9, "messages": 12480, "seats": 3 }, "limits": { "maxNumbers": 9, "monthlyMessages": 45000, "maxSeats": 5 } } ``` ## Errors ```text title="Examples" 401 { "error": "missing_bearer_token" } // no Authorization header 401 { "error": "invalid_token" } // unknown / revoked key, or inactive account 403 { "error": "account_deactivated", "message": "Deactivated Account" } // that business is suspended: its own key stops // working, and any request naming it is refused // — including with an account-wide key 400 { "error": "missing_fields", "message": "…" } 400 { "error": "missing_content", "message": "…" } // no content field on a message send 400 { "error": "invalid_media", "message": "…" } // media without an assetId, id or link 402 { "error": "plan_limit_media", "message": "…" } // plan storage cap reached 404 { "error": "media_not_found", "message": "…" } // unknown assetId, or past its 30 days 402 { "error": "plan_limit_numbers" } // every number the plan paid for is in use 402 { "error": "plan_limit_messages" } // the period's message allowance is exhausted 402 { "error": "subscription_past_due" } // trial or paid period lapsed without renewal 404 { "error": "connection_not_found" } 404 { "error": "template_not_found" } 409 { "error": "template_exists" } 413 { "error": "media_too_large", "message": "…" } // template header above 4 MB 422 { "error": "meta_rejected", "message": "…", "meta": { … } } // Meta read the payload and refused it 502 { "error": "meta_rejected", "message": "…" } // Graph unreachable or failing ``` ### What Meta refused returns 422, not 502 When Graph reads a payload and refuses it — a malformed template, a variable with no example, an unprovisioned number — the response is a **422**, and its body carries a `meta` object with Meta's own error code, its `traceId` and its class: ```json title="422 — refused by Meta" { "error": "meta_rejected", "message": "Invalid parameter", "meta": { "errorClass": "template", "retryable": false, "code": 100, "details": "body_text example count does not match the number of variables", "traceId": "AbC…" } } ``` `traceId` is what Meta support asks for: quote it verbatim. A **502** is left for what the word describes — Graph unreachable, or Graph itself failing. Returning a refused payload as a 502 made it undiagnosable: our CDN answers origin 5xx with its own error page, replacing that JSON body with the single line `error code: 502`. The reason for the refusal never reached you. 4xx bodies pass through untouched. ## Request logs Every response carries an `x-request-id` header. The same id identifies the call in your dashboard under Logs, where each request is kept with its status, duration, body and — when it failed — the response we returned. Webhook deliveries are listed next to it, with their exact signed payload and a resend button. How far back the log goes depends on your plan: 7 days on Starter, 30 on Growth, 90 on Scale. > [!NOTE] > **Redacted fields** > > Bodies are stored to make a failure reproducible, minus anything that looks like a credential — > `password`, `token`, `secret` and `api_key` values are replaced before the row is written. --- # TypeScript library URL: https://wa.genuka.com/en/docs/library Language: English > @genuka/whatsapp — typed builders, validation and event normalization. `@genuka/whatsapp` is not another HTTP client. It is the layer that prevents avoidable 400s, makes webhook handling exhaustive at compile time, and turns Meta's \~200 error codes into seven actionable decisions. > [!NOTE] > The library works standalone against Meta's Cloud API, with no Genuka account. It ships under > the MIT license. ## Installation ```bash npm install @genuka/whatsapp ``` ## Building a message Lengths, cardinalities and phone number format are checked **before** the network call. ```ts import { messages } from "@genuka/whatsapp"; const choice = messages.buttons("+237 6 99 00 11 22", { body: "How can we help?", buttons: [ { id: "order", title: "My order" }, { id: "support", title: "A problem" }, ], }); ``` A construction error is thrown immediately, with the offending field path: ```txt ValidationError: interactive.buttons: must contain at most 3 items (got 4) ``` ## The 24-hour window ```ts import { sendStrategy } from "@genuka/whatsapp"; sendStrategy(contact.lastInboundAt); // "free_form" | "template" ``` A function call, rather than a `131047` discovered after the attempt was already billed. ## Handling a webhook Meta nests everything in `entry[].changes[].value`, where the real discriminator is not a field but the *presence* of `messages` or `statuses`. The library flattens that once and for all. ```ts import { parseWebhook, eventKey } from "@genuka/whatsapp/webhooks"; for (const event of parseWebhook(await request.json())) { if (await alreadyProcessed(eventKey(event))) continue; switch (event.kind) { case "message": // inbound case "status": // sent / delivered / read / failed / deleted case "template": // approval, quality, recategorization case "account": // number quality, limits, suspension case "user_preference": // marketing opt-out case "coexistence": // history, echoes, contacts case "unknown": // never lost, always forwarded } } ``` The parser **never throws**: a malformed payload yields an empty list. An exception here would become a 500, and Meta would replay the whole batch. ## Deciding what to do with an error `errorClass` carries the decision, not the numeric code. ```ts import { WhatsAppError } from "@genuka/whatsapp"; try { await send(payload); } catch (error) { if (!(error instanceof WhatsAppError)) throw error; switch (error.errorClass) { case "needs_template": return resendAsTemplate(); // 131047 case "recipient_permanent": return markUnreachable(); // 131026, 131050 case "recipient_throttled": return requeueLater(); // 131049 case "media": return reuploadAndRetryOnce(); // 131052 case "retryable": return backoff(); // 4, 130429, 5xx case "config": return alertOperator(); // 190, 133010 case "template": return surfaceToCustomer(); // 132xxx case "validation": case "unknown": throw error; } } ``` ## Two transports, one core The same built payload can travel two routes, with the same builders, validation and error taxonomy: ```ts // With a Genuka WA key — no Meta token involved import { GenukaTransport } from "@genuka/whatsapp"; const transport = new GenukaTransport({ apiKey: process.env.GENUKA_WA_API_KEY! }); // Straight to Meta, if you hold your own credentials import { MetaTransport } from "@genuka/whatsapp"; const transport = new MetaTransport({ accessToken: process.env.META_TOKEN! }); ``` ## Full reference Every module has its own guide, sourced from the package itself — they live next to the code they describe, so they do not drift: * [Sending messages](https://wa.genuka.com/sdk/messages) * [Templates](https://wa.genuka.com/sdk/templates) * [Media](https://wa.genuka.com/sdk/media) * [Webhooks](https://wa.genuka.com/sdk/webhooks) * [Flows](https://wa.genuka.com/sdk/flows) * [Management](https://wa.genuka.com/sdk/management) * [Coexistence](https://wa.genuka.com/sdk/coexistence) ## Graph API version The library pins a Graph API version explicitly rather than following "latest". A silent version bump is the best way to discover a breaking change in production: changing `DEFAULT_GRAPH_VERSION` is a deliberate release. --- # WhatsApp Cloud API error codes: causes and fixes URL: https://wa.genuka.com/en/docs/errors Language: English > WhatsApp Cloud API error codes explained: what 131047, 131026, 130429, 132001, 141010 and the others mean, and whether to retry. A WhatsApp error code is the number Meta returns when the Cloud API refuses a call or cannot deliver a message. With Genuka WA, you read it in the `meta.code` field of the error response, or in `data.errors[].code` of the `failed` status webhook. The tables below give, for each code, what it means and whether to retry. *Last updated October 8, 2026* ## Which WhatsApp error codes are documented here? The families follow [Meta's official list](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) (authorization, throttling, integrity, other errors), with the "other errors" grouped here by topic. Meta's messages are quoted verbatim, as they arrive in API responses. ### Authorization and access tokens | Code | Meta's message | What it means | Retry? | | -------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | [0](https://wa.genuka.com/en/docs/errors/0) | "We were unable to authenticate the app user." | The access token expired, was invalidated, or its owner cut off app access. | No: a new token is needed. With Genuka WA that token is ours: contact support. | | [3](https://wa.genuka.com/en/docs/errors/3) | "Capability or permissions issue." | The app lacks the capability or permission this specific endpoint requires. | No: grant the missing permission. | | [10](https://wa.genuka.com/en/docs/errors/10) | "Permission is either not granted or has been removed." | A permission is missing. Common case: an OTP template for a business Meta has not verified. | No. | | [190](https://wa.genuka.com/en/docs/errors/190) | "Your access token has expired." | The access token expired or is no longer valid. | No: replace the token. With Genuka WA you never handle a Meta token. | | [200](https://wa.genuka.com/en/docs/errors/200) | "Permission is either not granted or has been removed." (range 200 to 299) | Missing token, revoked permission, or a system user with no access to the account. Unrelated to HTTP status 200. | No: restore the access. | ### Throttling | Code | Meta's message | What it means | Retry? | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------- | | [4](https://wa.genuka.com/en/docs/errors/4) | "The app has reached its API call rate limit." | The whole app went over its Meta API call limit. | Yes, later, with fewer calls. | | [80007](https://wa.genuka.com/en/docs/errors/80007) | "The WhatsApp Business Account has reached its rate limit." | Too many management calls (templates, numbers) on one WhatsApp Business account within the hour. | Yes, later. | | [130429](https://wa.genuka.com/en/docs/errors/130429) | "Cloud API message throughput has been reached." | The number is sending faster than its message throughput. | Yes, with growing delays. | | [131048](https://wa.genuka.com/en/docs/errors/131048) | "Message failed to send because there are restrictions on how many messages can be sent from this phone number…" | Meta restricts the number's sends after too many blocks or spam reports. | Only later, once your sends are cleaned up. | | [131056](https://wa.genuka.com/en/docs/errors/131056) | "Too many messages sent from the sender phone number to the same recipient phone number in a short period of time." | Too many messages to the same recipient in a short time. | Yes, to that recipient, after a pause. | ### Integrity and account health | Code | Meta's message | What it means | Retry? | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | | [368](https://wa.genuka.com/en/docs/errors/368) | "The WhatsApp Business Account associated with the app has been restricted or disabled for violating a platform policy." | The WhatsApp Business account is restricted or disabled for breaking a Meta policy. | No: request a review from Meta. | | [141010](https://wa.genuka.com/en/docs/errors/141010) | "The Business has not passed business verification" | The business has not passed Meta verification, so its OTP templates are refused. This code shows up in the account's health status (`health_status`), not in Meta's list of error codes. | No: get the business verified. | ### Request and phone number | Code | Meta's message | What it means | Retry? | | -------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | --------------------------------------- | | [33](https://wa.genuka.com/en/docs/errors/33) | "The business phone number has been deleted." | The phone number ID you use no longer points to an active number. | No: check the ID, reconnect the number. | | [100](https://wa.genuka.com/en/docs/errors/100) | "The request included one or more unsupported or misspelled parameters." | A parameter is unknown, misspelled or too long. The cause is usually in `details`. | No: fix the request. | ### Delivery and payment | Code | Meta's message | What it means | Retry? | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | [131026](https://wa.genuka.com/en/docs/errors/131026) | "Unable to deliver message. Reasons can include: The recipient phone number is not a WhatsApp phone number…" | The recipient is not on WhatsApp, has not accepted the terms, or runs an app that is too old. | No: check the number, use another channel. | | [131042](https://wa.genuka.com/en/docs/errors/131042) | "There was an error related to your payment method." | The WhatsApp Business account has no valid payment method with Meta, which bills the client directly. | Once the payment method is added or fixed with Meta. | | [131047](https://wa.genuka.com/en/docs/errors/131047) | "More than 24 hours have passed since the recipient last replied to the sender number." | The 24-hour customer service window is closed: only an approved template can go out. | No: resend the content as a template. | | [131049](https://wa.genuka.com/en/docs/errors/131049) | "This message was not delivered to maintain healthy ecosystem engagement." | Meta held back a marketing template, most often because the recipient hit their marketing limit. | Not before 24 hours (US numbers are a separate case, see the page). | | [131050](https://wa.genuka.com/en/docs/errors/131050) | "Unable to deliver the message. This recipient has chosen to stop receiving marketing messages on WhatsApp from your business." | The recipient stopped your marketing messages. | Never. | ### Templates and Flows | Code | Meta's message | What it means | Retry? | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | | [132000](https://wa.genuka.com/en/docs/errors/132000) | "The number of variable parameter values included in the request did not match the number of variable parameters defined in the template." | The number of values sent does not match the template's variables. | No: send one value per variable. | | [132001](https://wa.genuka.com/en/docs/errors/132001) | "The template does not exist in the specified language or the template has not been approved." | No approved template with that name in that language. Common trap: `en` and `en_US` are two different templates. | No: check the name, language and status. | | [132015](https://wa.genuka.com/en/docs/errors/132015) | "Template is paused due to low quality so it cannot be sent in a template message." | Meta paused the template for low quality. | Once the pause lifts, or with another template. | | [132069](https://wa.genuka.com/en/docs/errors/132069) | "Flow is in throttled state and 10 messages using this flow were already sent in the last hour." | The Flow is capped at 10 sends per hour because its endpoint is unhealthy. | Once the endpoint is fixed. | ## Where do I find the error code in Genuka WA? In two places, depending on when Meta refuses. **During the call.** When Meta refuses the request, the Genuka WA error response carries a `meta` object: Meta's code, its class, `retryable` saying whether another attempt makes sense, and `traceId`, the identifier Meta support asks for. Example from the [API reference](https://wa.genuka.com/en/docs/api): ```json title="422 — refused by Meta" { "error": "meta_rejected", "message": "Invalid parameter", "meta": { "errorClass": "template", "retryable": false, "code": 100, "details": "body_text example count does not match the number of variables", "traceId": "AbC…" } } ``` **After the send.** An accepted message can still fail to deliver (131026, 131049, 131050…). The failure then reaches your [webhooks](https://wa.genuka.com/en/docs/webhooks): a `message_status` event, status `failed`, with Meta's code in `data.errors`: ```json title="message_status webhook (excerpt)" { "type": "message_status", "field": "messages", "connection_id": "con_1", "data": { "id": "wamid.HBgLMjM3...", "status": "failed", "recipient_id": "237690000001", "errors": [{ "code": 131026, "title": "…", "message": "…" }] } } ``` ## My code is not in this list: where do I look? Meta publishes the full list of Cloud API codes, with its suggested fix for each: [Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes). The `meta.details` field carries the detail Meta gave for your request. If the refusal still makes no sense, send support the `meta.traceId` and the response's `x-request-id` header. ## FAQ ### What is the difference between errors 4, 80007 and 130429? They do not limit the same thing. Error [4](https://wa.genuka.com/en/docs/errors/4) caps the whole app's calls, [80007](https://wa.genuka.com/en/docs/errors/80007) the management calls on one WhatsApp Business account (templates, numbers), and [130429](https://wa.genuka.com/en/docs/errors/130429) the message throughput of one number. All three are retried later, with growing delays. ### Should I retry WhatsApp errors automatically? Only when `meta.retryable` is `true`, and with growing delays. Authorization, template and recipient errors fail the same way until their cause is fixed. One special case: [131048](https://wa.genuka.com/en/docs/errors/131048) is marked retryable, but that means "later", not "right now". ### Why "Application does not have permission for this action" on an OTP template? Because the business has not passed Meta verification: its authentication templates are refused, and the account's health status carries error [141010](https://wa.genuka.com/en/docs/errors/141010). The message blames the app by mistake. See also [error 10](https://wa.genuka.com/en/docs/errors/10). ### Does error 131047 mean my number is blocked? No. It only means the contact has not written to you in more than 24 hours. Resend the content as an approved template; free-form conversation resumes as soon as they reply. ## Sources Read on October 8, 2026. * Meta — [Error codes (Cloud API)](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * Meta — [Health status](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/health-status) --- # WhatsApp error 0: unable to authenticate — fix URL: https://wa.genuka.com/en/docs/errors/0 Language: English > WhatsApp error 0 "We were unable to authenticate the app user": expired or invalidated token, or access cut off. How to fix it, and the Genuka WA case. WhatsApp error 0 means Meta could not authenticate the user behind the access token. In practice, the token has expired, been invalidated, or its owner has stopped apps from accessing their data. You fix it the same way as error 190: with a new token, ideally a system user token that does not depend on any one person. ## What does error 0 mean? It is the first row of the Cloud API's authorization errors: > "We were unable to authenticate the app user." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#authorization-errors) Meta explains: "Typically this means the included access token has expired, been invalidated, or the app user has changed a setting to prevent all apps from accessing their data." The suggested fix is to get a new access token, pointing to system user tokens. The general Graph documentation describes the same symptom for `OAuthException` errors without a subcode: the login status or token has expired, been revoked or is otherwise invalid ([Meta, Graph API error handling](https://developers.facebook.com/docs/graph-api/guides/error-handling)). ### Error 0 or error 190? | | Error 0 | Error 190 | | ---------------- | ---------------------------------------------- | --------------------------------- | | Meta's wording | "We were unable to authenticate the app user." | "Your access token has expired." | | What is at fault | The user behind the token, or the token itself | The token, expired or invalidated | | Meta's remedy | A new access token | A new access token | Either way, the same request with the same token will always fail. ## When does error 0 happen? * **The token expired or was invalidated**, for instance a user token generated from the App Dashboard's API Setup page, which Meta expires within a few hours ([Meta, Access tokens](https://developers.facebook.com/documentation/business-messaging/whatsapp/access-tokens#user-access-tokens)). * **The token depends on a person who changed their settings.** A user token is tied to the Facebook account of whoever generated it: if they cut off app access to their data, the token stops working. * **The token was revoked** on Meta's side. ### Where do you see it in Genuka WA? Genuka WA puts error 0 in the `config` class and treats it as a dead credential: in a `MARKETING` campaign it does not trigger the fallback from the Marketing Messages API to `/messages`, which would only double the failure and hide the real cause. ```json title="409 — POST /api/v1/messages" { "error": "send_config", "message": "We were unable to authenticate the app user.", "meta": { "errorClass": "config", "retryable": false, "code": 0, "traceId": "AbC…" } } ``` On `POST /api/v1/templates`, the same error comes back as `403 meta_rejected`. ## How do I fix error 0? ### If you go through Genuka WA You have no Meta token to renew: every call on your number goes out with Genuka's system user token, which depends on no person and has no expiry date. An error 0 is therefore Genuka's to fix. 1. **Stop retrying.** `retryable: false`: nothing changes until the token does. 2. **Contact Genuka support** with the `x-request-id` header and `meta.traceId`. 3. **Check the number** with `GET /api/v1/connections`. `disconnected` means Meta reported the account as no longer shared with Genuka, or deleted, or that you released the number yourself. A customer who removed access restores it through the [connect link](https://wa.genuka.com/en/docs/onboarding); a released number needs a free slot again; a deleted account, or one disabled by Meta ([error 368](https://wa.genuka.com/en/docs/errors/368)), is not restored that way. The `account_update` event forwarded to your webhooks tells you which case applies. ### If you call the Cloud API with your own token 1. **Run the token through the debugger.** The [access token debugger](https://developers.facebook.com/tools/debug/accesstoken/) shows whether it is still valid, who owns it and which permissions it carries. 2. **Replace it with a system user token.** Meta describes them as long-lived and able to represent an automated service with no user input, unlike user tokens ([Meta, Access tokens](https://developers.facebook.com/documentation/business-messaging/whatsapp/access-tokens#system-user-access-tokens)). Generate it with `business_management`, `whatsapp_business_management` and `whatsapp_business_messaging`. 3. **Assign it the accounts.** Without access to the WhatsApp account and the Messaging account, the new token gets an [error 200](https://wa.genuka.com/en/docs/errors/200) instead of 0. ## How do I prevent error 0? * **No personal tokens in production.** An employee leaving, a changed password or a privacy setting must not be able to stop your sends. * **Watch the `config` class.** In Genuka WA, a sudden rise of `meta.errorClass: "config"` responses across several numbers at once points to a credential problem, not a content problem. * **Keep the `traceId`.** It is what Meta support asks for, and the Graph documentation says the ID expires shortly. ## Related error codes * [190](https://wa.genuka.com/en/docs/errors/190): the token has expired. * [200](https://wa.genuka.com/en/docs/errors/200): the token is valid but lacks access to the account or permission. * [10](https://wa.genuka.com/en/docs/errors/10): permission not granted or removed. * [3](https://wa.genuka.com/en/docs/errors/3): capability or permission missing for this endpoint. ## FAQ ### Should I retry a call refused with error 0? No. The token is at fault: the same request will fail identically until it is replaced. ### Is my Genuka API key compromised? No. Error 0 is about the Meta token, not your `pk_live_…` key. A refused key produces a Genuka `401 invalid_token`. ### Why did my token work yesterday? Because a user token expires within hours, or stops working as soon as its owner changes their settings. A system user token does not have that weakness. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Access tokens](https://developers.facebook.com/documentation/business-messaging/whatsapp/access-tokens) * [Meta — Graph API, Handling errors](https://developers.facebook.com/docs/graph-api/guides/error-handling) --- # WhatsApp error 3: capability or permission missing URL: https://wa.genuka.com/en/docs/errors/3 Language: English > WhatsApp Cloud API error 3 (Capability or permissions issue): how it differs from errors 0, 10, 190 and 200, and how to fix it. WhatsApp error 3 means the application calling the Cloud API lacks the capability or permission that this specific endpoint requires. The token can be perfectly valid: it is the app that is not allowed to make that particular call. You fix it by granting the missing permission or feature, never by sending the same request again. ## What does error 3 mean? Meta lists it among the Cloud API's authorization errors: > "Capability or permissions issue." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#authorization-errors) The suggested fix: check in the access token debugger that the app was granted the permissions the endpoint requires. The general Graph documentation names this code "API Method" and sums it up as "Make sure your app has the necessary capability or permissions to make this call" ([Meta, Graph API error handling](https://developers.facebook.com/docs/graph-api/guides/error-handling)). ### How do you tell it apart from the other authorization errors? The Cloud API's six authorization codes each answer a different question. The wording is from [Meta's error page](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#authorization-errors): | Code | Meta's wording | What is missing | | -------------------------- | ------------------------------------------------------- | ----------------------------------------------------- | | [0](https://wa.genuka.com/en/docs/errors/0) | "We were unable to authenticate the app user." | An authenticable user behind the token | | [190](https://wa.genuka.com/en/docs/errors/190) | "Your access token has expired." | A token that is still valid | | **3** | "Capability or permissions issue." | An app capability or permission for this endpoint | | [10](https://wa.genuka.com/en/docs/errors/10) | "Permission is either not granted or has been removed." | A permission, or eligibility for the API being called | | [200](https://wa.genuka.com/en/docs/errors/200) | "No access token was provided." | A token in the request | | 200 to 299 | "Permission is either not granted or has been removed." | A permission, or the user's access to the account | Keep the boundary in mind: with 0 and 190 the token itself is dead; with 200 ("No access token was provided") there is no token at all; with 3, 10 and 201 to 299 the token is alive but does not give access to what you are asking for. ## When does error 3 happen? * **The token was generated without the WhatsApp permissions**, or for a different app from the one making the call. When generating it, Meta asks you to select the app used for the calls and the `whatsapp_business_management` and `whatsapp_business_messaging` permissions ([Meta, WhatsApp support](https://developers.facebook.com/documentation/business-messaging/whatsapp/support#authentication-authorization)). * **The endpoint belongs to a feature the app or account has not enabled.** The Marketing Messages API is one example: it requires onboarding completed by a portfolio user with full control (same page). Until that onboarding is done, the call has no reason to succeed. ### Where do you see it in Genuka WA? Genuka WA puts code 3 in the `config` class: a token, permission or registration problem that a retry will not fix. | Channel | What you get | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `POST /api/v1/messages` | `409`, `"error": "send_config"`, `meta.code: 3` | | `POST /api/v1/templates` | `403`, `"error": "meta_rejected"`, `meta.code: 3` | | `MARKETING` campaign | If the Marketing Messages API refuses the send with a 4xx permission or parameter error (codes 3, 10, 100…), Genuka resends the message through the regular `/messages` endpoint; if that refuses too, the recipient turns `failed` | The last case is deliberate. Meta exposes no flag that says beforehand whether an account has access to the Marketing Messages API: Genuka tries it, treats a 4xx permission-type refusal as "feature unavailable on this account" and resends the message the regular way. The fallback is silent: only the result of the send on `/messages` is recorded. It does not cover every refusal: Meta also reports ineligibility with code `134102` and an HTTP 500 status ([Meta, Marketing Messages API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#marketing-messages-api-for-whatsapp-error-codes)); Genuka treats it as a transient outage, retries it, then marks the recipient `failed`. ## How do I fix error 3? ### If you call the Cloud API with your own token 1. **Inspect the token** in the [access token debugger](https://developers.facebook.com/tools/debug/accesstoken/): which app does it belong to, and does it carry `whatsapp_business_management` and `whatsapp_business_messaging`? 2. **Regenerate it if needed**, selecting the right app and both WhatsApp permissions. For a production service, prefer a system user token ([Meta, Access tokens](https://developers.facebook.com/documentation/business-messaging/whatsapp/access-tokens#system-user-access-tokens)). 3. **Check the requirements of the feature you are calling.** If the endpoint belongs to a feature that requires onboarding or eligibility, such as the Marketing Messages API, complete that first before trying again. ### If you go through Genuka WA You have no Meta token to fix: a code 3 on a send or a template creation is Genuka's to handle. Contact support with the response's `x-request-id` header and `meta.traceId`. If you are sending an advanced message type through the `raw` field, say so: that is where a feature that is not enabled is most likely to show up. ## How do I prevent error 3? * **Generate every token for the right app**, with only the WhatsApp permissions you need. * **Before adopting a new Meta feature**, read its access requirements: many need their own onboarding or eligibility. * **Alert on the `config` class**, not on the message text: Meta warns that error titles will eventually be deprecated. ## Related error codes * [10](https://wa.genuka.com/en/docs/errors/10): permission not granted or removed, or API not eligible. * [200](https://wa.genuka.com/en/docs/errors/200): no token, or a user without access to the account. * [190](https://wa.genuka.com/en/docs/errors/190): the token has expired. * [0](https://wa.genuka.com/en/docs/errors/0): Meta could not authenticate the app user. ## FAQ ### What is the difference between error 3 and error 10? Meta ties 3 to a capability or permission missing for the endpoint, and 10 to a permission not granted or removed, including when the account is not eligible for the API being called. In both cases the token is alive; what it authorizes is not enough. ### Should I retry a call refused with error 3? No. As long as the permission or feature is missing, the same request will fail the same way. ### Can a marketing campaign recipient fail with error 3? Yes. A refusal from the Marketing Messages API alone never reaches you: Genuka resends the message through `/messages`, and that second send is the one that counts. If the recipient still turns `failed` with a permission reason, `/messages` refused it too: the problem affects the number, not just the Marketing Messages API, and is one for Genuka support. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Graph API, Handling errors](https://developers.facebook.com/docs/graph-api/guides/error-handling) * [Meta — WhatsApp support](https://developers.facebook.com/documentation/business-messaging/whatsapp/support) * [Meta — Access tokens](https://developers.facebook.com/documentation/business-messaging/whatsapp/access-tokens) --- # WhatsApp error 10: permission denied — cause and fix URL: https://wa.genuka.com/en/docs/errors/10 Language: English > WhatsApp error 10 "Application does not have permission for this action": a missing permission, or an OTP template on an unverified business. WhatsApp error 10 means a permission the call needs was never granted or has been removed. On the Cloud API, the most common case misleads everyone: creating an `AUTHENTICATION` (OTP) template for a business Meta has not verified returns "Application does not have permission for this action". The problem is the business, not the application. ## What does error 10 mean? Meta lists it among the authorization errors: > "Permission is either not granted or has been removed." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#authorization-errors) Meta's suggested fix covers three leads: check in the access token debugger that the app was granted the permissions the endpoint requires; for WhatsApp Flows with an endpoint, check that the phone number used to set the business public key is allowlisted; and check the eligibility requirements of the API you are calling, because an account that is not eligible gets exactly this code. The general Graph documentation calls code 10 "API Permission Denied" ([Meta, Graph API error handling](https://developers.facebook.com/docs/graph-api/guides/error-handling)). ## When does error 10 happen? | Situation | What you see | Who can fix it | | ----------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------- | | Creating an `AUTHENTICATION` template for an unverified business | "Application does not have permission for this action" | The business: Meta business verification, then a messaging limit moved to 2,000 | | Token without `whatsapp_business_management` or `whatsapp_business_messaging` | The same message, on any endpoint | Whoever owns the token | | Feature reserved to eligible accounts | Code 10 on that feature's endpoint | Depends on Meta's requirements for that API | | WhatsApp Flows with an endpoint, number not allowlisted | Code 10 when setting the public key | The app owner | ### The OTP template case This is the one that costs hours. On the accounts connected to Genuka WA, we see that: * creating an `AUTHENTICATION` template fails with "Application does not have permission for this action" when the business has not passed Meta business verification; * `MARKETING` and `UTILITY` templates on the same account are created normally; * the account's health status carries error [141010](https://wa.genuka.com/en/docs/errors/141010), "The Business has not passed business verification". A developer reported the same response on Meta's forum, with `error_subcode: 2388185` and the user message "This WhatsApp Business account does not have permission to create message template" ([Meta developer forum](https://developers.facebook.com/community/threads/1372007877817645/)). The message blames the application. No token or permission setting changes anything. You cannot work around it with a utility template either: when an app offers users one-time passwords or verification codes over WhatsApp, Meta requires an authentication template ([Meta, Authentication templates](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/authentication-templates/authentication-templates)). ### Where do you see it in Genuka WA? Genuka WA puts code 10 in the `config` class: token, permission or registration, nothing a retry can fix. | Channel | What you get | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `POST /api/v1/templates` | `403`, `"error": "meta_rejected"`, `meta.code: 10` | | `POST /api/v1/messages` | `409`, `"error": "send_config"`, `meta.code: 10` | | `MARKETING` campaign | If the Marketing Messages API refuses with this code, Genuka resends the message through the regular `/messages` endpoint; if that refuses too, the recipient turns `failed` | ```json title="403 — POST /api/v1/templates (AUTHENTICATION category)" { "error": "meta_rejected", "message": "Application does not have permission for this action", "meta": { "errorClass": "config", "retryable": false, "code": 10, "traceId": "AbC…" } } ``` ## How do I fix error 10? ### On an authentication template 1. **Confirm the cause.** In Meta Business Suite, **Security Center > Business Verification** shows the verification status of the portfolio that owns the WhatsApp account. If you call Graph yourself, `GET /{WABA_ID}?fields=business_verification_status,health_status` answers in one request: `business_verification_status` is described in the [WhatsApp Business Account reference](https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/whatsapp-business-account-api), `health_status` in [Health status](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/health-status). 2. **Get the business verified.** It starts from Meta Business Suite, on the portfolio that owns the WhatsApp account ([Meta, Verify your business](https://www.facebook.com/business/help/2058515294227817)). Details are on the [error 141010](https://wa.genuka.com/en/docs/errors/141010) page. 3. **Recreate the template once the business is verified and the limit has moved to 2,000** (WhatsApp Manager, **Account tools > Messaging limits**). The same `POST /api/v1/templates`, with `"category": "AUTHENTICATION"` — see the [OTP codes from Node.js](https://wa.genuka.com/en/docs/guides/whatsapp-otp-nodejs) guide. ### On a missing permission * **If you own the token**: open it in the [access token debugger](https://developers.facebook.com/tools/debug/accesstoken/), check `whatsapp_business_management` and `whatsapp_business_messaging`, and regenerate it with both if one is missing ([Meta, WhatsApp support](https://developers.facebook.com/documentation/business-messaging/whatsapp/support#authentication-authorization)). * **If you go through Genuka WA**: you have no Meta token to fix. A code 10 outside an authentication template is Genuka's to handle: contact support with the `x-request-id` header and `meta.traceId`. ## How do I prevent error 10? * **Get the business verified before planning WhatsApp OTPs.** It is a Meta prerequisite, not a Genuka option. * **Test the authentication flow on a verified account** before you announce the feature: an unverified test account will always fail. * **Keep both WhatsApp permissions** on any token you generate yourself. ## Related error codes * [141010](https://wa.genuka.com/en/docs/errors/141010): the business has not passed Meta business verification. * [200](https://wa.genuka.com/en/docs/errors/200): the token has no access to the account, or a permission is missing (200 to 299). * [3](https://wa.genuka.com/en/docs/errors/3): capability or permission missing for this endpoint. * [190](https://wa.genuka.com/en/docs/errors/190): the token has expired. ## FAQ ### Why does Meta blame the application when my token is fine? Because the message is generic. On an `AUTHENTICATION` template we see it when the business is not verified, while the token and its permissions are correct: marketing and utility templates on the same account go through. ### Can I send OTP codes with a utility template in the meantime? No. Meta requires an authentication template for one-time codes. Until verification is done, send your codes through another channel, SMS or email. ### Should I regenerate my Genuka API key? No. Your Genuka key plays no part in Meta permissions. A key problem produces a Genuka `401`, never a code 10. ### Does code 10 also block my sends? Not in the OTP template case: only creating `AUTHENTICATION` templates is refused. Your approved marketing and utility templates keep going out. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Graph API, Handling errors](https://developers.facebook.com/docs/graph-api/guides/error-handling) * [Meta — Authentication templates](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/authentication-templates/authentication-templates) * [Meta — Health status](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/health-status) * [Meta — WhatsApp Business Account API](https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/whatsapp-business-account-api) * [Meta — Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits) * [Meta — WhatsApp support, authentication and authorization errors](https://developers.facebook.com/documentation/business-messaging/whatsapp/support#authentication-authorization) * [Meta — Verify your business in Meta Business Suite](https://www.facebook.com/business/help/2058515294227817) * [Meta developer forum — error 10 on an authentication template](https://developers.facebook.com/community/threads/1372007877817645/) --- # WhatsApp error 190: access token expired — cause and fix URL: https://wa.genuka.com/en/docs/errors/190 Language: English > WhatsApp Cloud API error 190 (access token expired): which token expired, how to replace it for good, and why Genuka WA users never handle Meta tokens. WhatsApp error 190 means the access token sent to Meta's Cloud API has expired or is no longer valid. The fix is a new token, and above all to stop using a temporary user token: a system user token does not die after a few hours. With Genuka WA, you never handle a Meta token at all. ## What does error 190 mean? Meta lists it among the Cloud API authorization errors: > "Your access token has expired." — suggested fix: "Get a new access token." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#authorization-errors) The general Graph API documentation, which the Cloud API is built on, names the same code "Access token has expired" and documents subcodes ([Meta, Graph API error handling](https://developers.facebook.com/docs/graph-api/guides/error-handling)): | Graph subcode | Meta's name | What it tells you | | ------------- | -------------------- | --------------------------------------------------------- | | `463` | Expired | The token expired, was revoked or is otherwise invalid | | `467` | Invalid Access Token | The token expired, was revoked or is otherwise invalid | | `460` | Password Changed | The person who generated the token changed their password | | `458` | App Not Installed | The user has not logged into your app | Do not build logic on those subcodes: the WhatsApp error page says `error_subcode` is deprecated and no longer returned from Graph v16.0 onward. The `190` code and the `details` field are enough. ## When does error 190 happen? * **You are using the API Setup token.** Meta's App Dashboard generates a fresh user token every time you open **WhatsApp > API Setup**. Meta meant it for first tests: user tokens "expire quickly", so you have to generate a new one every few hours ([Meta, Access tokens](https://developers.facebook.com/documentation/business-messaging/whatsapp/access-tokens)). It is the usual reason a service works in the morning and fails in the afternoon. * **The token was invalidated.** Password changed, app removed by the user, token revoked: Meta answers 190 even before the expiry date. * **You are a partner and one customer's token died.** At the end of Embedded Signup, a Tech Provider receives a business integration system user token scoped to that customer. We have seen it stop working as soon as the customer edits the partner's access in their Business Manager: Meta then answers 190 "Authentication Error" on every send, for a number that is otherwise healthy. ## How do I fix error 190? ### If you call the Cloud API with your own token 1. **Inspect the token.** Paste it into [Meta's access token debugger](https://developers.facebook.com/tools/debug/accesstoken/): it shows the expiry and the permissions. It needs `whatsapp_business_management` and `whatsapp_business_messaging` ([Meta, WhatsApp support](https://developers.facebook.com/documentation/business-messaging/whatsapp/support#authentication-authorization)). 2. **Replace a user token with a system user token.** In Business settings, **System Users**: create a system user, give it control of your app, then **Generate token**, picking the app, an expiration preference and the `business_management`, `whatsapp_business_management` and `whatsapp_business_messaging` permissions ([Meta, Access tokens](https://developers.facebook.com/documentation/business-messaging/whatsapp/access-tokens#system-user-access-tokens)). 3. **Give it access to the accounts.** A system user without access to the target WhatsApp account no longer gets 190 but [200](https://wa.genuka.com/en/docs/errors/200): assign it the WhatsApp account and the Messaging account in Meta Business Suite, from each account's **People** tab (same page). 4. **Store it as an opaque secret.** Meta warns that token formats can change: no fixed column length, no decoding. ### If you go through Genuka WA There is nothing for you to regenerate. Genuka WA holds the Cloud API access: every call on your number goes out with Genuka's system user token, created with no expiry date, rather than with the token Embedded Signup returned when the number was connected. Genuka made that choice precisely to avoid the partner case described above. A `190` in a Genuka WA response is therefore, in the vast majority of cases, an incident on Genuka's side. It comes back like this: ```json title="409 — POST /api/v1/messages" { "error": "send_config", "message": "Authentication Error", "meta": { "errorClass": "config", "retryable": false, "code": 190, "traceId": "AbC…" } } ``` When creating a template, the same refusal comes back as `403 meta_rejected` with the same `meta` object ([API reference](https://wa.genuka.com/en/docs/api#errors)). Either way: 1. **Do not retry in a loop.** `retryable: false`: the same call will fail the same way. 2. **Contact Genuka support** with the response's `x-request-id` header and `meta.traceId`. 3. **Do not ask the customer to reconnect straight away.** Check `GET /api/v1/connections` first. `disconnected` means Meta reported the account as no longer shared with Genuka, or deleted, or that you released the number yourself. A customer who removed access restores it through the [connect link](https://wa.genuka.com/en/docs/onboarding), without using a new slot; a released number needs a free slot again; a deleted account, or one disabled by Meta ([error 368](https://wa.genuka.com/en/docs/errors/368)), is not restored that way, and the `account_update` event forwarded to your webhooks tells you which case applies. If it still reads `connected`, stay with Genuka support. Do not confuse it with errors on your own API key, which never come from Meta: `401 missing_bearer_token` (no header) and `401 invalid_token` (unknown or revoked key), see [Authentication](https://wa.genuka.com/en/docs/authentication). **Node.js** ```ts const response = await fetch("https://wa.genuka.com/api/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ connectionId: "con_1", to: "+237690000001", template: { name: "order_confirmation", language: "en_US", variables: ["Awa", "ORD-1042"] }, }), }); const json = await response.json(); if (response.status === 401) { // Your Genuka key: missing, unknown or revoked. Nothing to do with Meta. throw new Error(`API key refused: ${json.error}`); } if (json.meta?.code === 190 || json.meta?.code === 0) { // Meta token refused: on Genuka's side. Stop and alert, do not retry. console.error("Meta token refused", { requestId: response.headers.get("x-request-id"), traceId: json.meta.traceId, }); } ``` **Python** ```python import os import requests response = requests.post( "https://wa.genuka.com/api/v1/messages", headers={"Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}"}, json={ "connectionId": "con_1", "to": "+237690000001", "template": {"name": "order_confirmation", "language": "en_US", "variables": ["Awa", "ORD-1042"]}, }, timeout=30, ) body = response.json() if response.status_code == 401: raise RuntimeError(f"API key refused: {body['error']}") # your Genuka key, not Meta meta = body.get("meta") or {} if meta.get("code") in (0, 190): # Meta token refused: on Genuka's side. Alert, do not retry. print("Meta token refused", response.headers.get("x-request-id"), meta.get("traceId")) ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => true, CURLOPT_USERAGENT => "acme-crm/1.0 (+https://example.com)", CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("GENUKA_WA_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "connectionId" => "con_1", "to" => "+237690000001", "template" => ["name" => "order_confirmation", "language" => "en_US", "variables" => ["Awa", "ORD-1042"]], ]), ]); $raw = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); curl_close($ch); $body = json_decode(substr($raw, $headerSize), true); preg_match('/x-request-id:\s*(\S+)/i', substr($raw, 0, $headerSize), $requestId); if ($status === 401) { throw new RuntimeException("API key refused: " . $body["error"]); // your Genuka key } if (in_array($body["meta"]["code"] ?? null, [0, 190], true)) { error_log("Meta token refused, request " . ($requestId[1] ?? "?") . ", trace " . ($body["meta"]["traceId"] ?? "?")); } ``` **curl** ```bash curl -i -X POST https://wa.genuka.com/api/v1/messages \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "to": "+237690000001", "template": { "name": "order_confirmation", "language": "en_US", "variables": ["Awa", "ORD-1042"] } }' # -i prints x-request-id: include it, with meta.traceId, when you contact support. ``` ## How do I prevent error 190? * **Never run production on the API Setup token.** It exists to send a first test message, not to power a service that runs overnight. * **One system user token per environment**, with only the WhatsApp permissions you need, kept in your secrets manager. * **Watch for revoked access.** As a partner, the `account_update` webhook reports `PARTNER_APP_UNINSTALLED` when a customer deauthenticates or uninstalls your app, and `PARTNER_REMOVED` when an account is no longer shared with you ([Meta, account\_update webhook](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/account_update)). Genuka WA forwards these events to your [webhooks](https://wa.genuka.com/en/docs/webhooks) (`account_update` event), but of the two, only `PARTNER_REMOVED` turns the number `disconnected`: after a `PARTNER_APP_UNINSTALLED`, it stays `connected`. * **Alert on the class, not on the text.** In Genuka WA, `meta.errorClass: "config"` covers token, permission and registration problems: a human has to look, and no retry will fix it. ## Related error codes * [0](https://wa.genuka.com/en/docs/errors/0): Meta could not authenticate the app user; same remedy. * [200](https://wa.genuka.com/en/docs/errors/200): the token is valid but lacks access to the account or permission. * [10](https://wa.genuka.com/en/docs/errors/10): permission not granted or removed. * [3](https://wa.genuka.com/en/docs/errors/3): capability or permission missing for this endpoint. ## FAQ ### How long does a WhatsApp Cloud API access token last? It depends on the type. A user token, like the one on the API Setup page, expires within a few hours. A system user token is long-lived, and you choose its expiration preference when you generate it. ### Should I retry a send that failed with 190? Not until the token has changed. The same token produces the same error; retrying only delays the alert. ### Does my customer need to reconnect after a 190 on Genuka WA? Only if their number shows `disconnected` in `GET /api/v1/connections` because they removed Genuka's access: the `account_update` event forwarded to your webhooks confirms it. A deleted account, or one disabled by Meta, is not restored by reconnecting. If the number still reads `connected`, contact Genuka support with `x-request-id` and `meta.traceId`. ### Should I regenerate my Genuka API key? No. Your `pk_live_…` key authenticates your calls to Genuka WA, not to Meta. A refused key produces a Genuka `401`, never a `190`. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Access tokens](https://developers.facebook.com/documentation/business-messaging/whatsapp/access-tokens) * [Meta — Graph API, Handling errors](https://developers.facebook.com/docs/graph-api/guides/error-handling) * [Meta — WhatsApp support, authentication and authorization errors](https://developers.facebook.com/documentation/business-messaging/whatsapp/support#authentication-authorization) * [Meta — account\_update webhook](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/account_update) --- # WhatsApp error 200: access denied (200-299) — fix URL: https://wa.genuka.com/en/docs/errors/200 Language: English > WhatsApp Cloud API error 200: missing token, revoked permission or a system user with no access to the account. How to diagnose and fix it. WhatsApp error 200, like the whole 200 to 299 range, means the call lacks the rights it needs: no token, a permission never granted or since removed, or a system user with no access to the target WhatsApp account. It has nothing to do with HTTP status 200. You fix it by restoring access, not by retrying. ## What does error 200 mean? Meta lists two entries among its authorization errors ([Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#authorization-errors)): | Code | `details` according to Meta | What Meta recommends | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | `200` | "No access token was provided." The API then answers "Provide valid app ID", on some `GET` endpoints such as `whatsapp_business_profile`; other endpoints return 190 or 104 instead | "Ensure your request includes a valid access token." Meta notes this is distinct from 190 | | `200` to `299` | "Permission is either not granted or has been removed." | Check in the access token debugger that the app has the permissions the endpoint requires | Meta's token guide adds the most common production case: most endpoints check that the user behind the token has access to the requested resource, and otherwise refuse with error code 200, "not to be confused with HTTP status code 200" ([Meta, Access tokens](https://developers.facebook.com/documentation/business-messaging/whatsapp/access-tokens#business-asset-access)). Graph calls this range "API Permission" ([Meta, Graph API error handling](https://developers.facebook.com/docs/graph-api/guides/error-handling)). ## When does error 200 happen? * **The system user is not assigned to the account.** An "employee" system user needs partial or full access to the WhatsApp account **and** the Messaging account: access to the latter alone is not enough (same page). * **The token lacks a permission.** Deregistering a number without `whatsapp_business_management` returns 200, for example ([Meta, Registration](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/registration)). * **The request has no token at all**, typically a `GET` on the business profile. * **The business stopped sharing its account with the partner.** Meta then emits a `PARTNER_REMOVED` event ([Meta, account\_update webhook](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/account_update)), and the partner's system user loses access to the account. ### Where do you see it in Genuka WA? With Genuka WA, the last case is a common one: your customer removed Genuka's access to their WhatsApp account, in their Business Manager or, for a coexistence number, from the WhatsApp Business app ([Meta, coexistence](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users/)). Genuka receives the `PARTNER_REMOVED`, marks the connection `disconnected` and forwards the `account_update` event to your [webhooks](https://wa.genuka.com/en/docs/webhooks). The same situation can also come back as code 100 with subcode 33, "… cannot be loaded due to missing permissions", see [error 100](https://wa.genuka.com/en/docs/errors/100). Genuka does not put codes 200 to 299 in a dedicated class: the class follows the HTTP status Meta returned, `config` on a 401 or 403, `unknown` otherwise. Either way, the refusal is not retryable. | Channel | What you get | | ------------------------- | -------------------------------------------------------------- | | `POST /api/v1/messages` | `409 send_config` or `400 send_unknown`, with `meta.code: 200` | | `POST /api/v1/templates` | `403` or `422`, `"error": "meta_rejected"` | | `GET /api/v1/connections` | `"status": "disconnected"` on the number concerned | A coexistence number can also turn `offboarded`: Meta reported that it is no longer linked to the API, for instance after a device change followed by a re-registration ([Meta, coexistence](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users/)). Genuka then refuses sends itself, before calling Meta: `409 send_config`, with a message naming the offboarding ("was offboarded by the merchant…") and no `meta.code`. A number you released from your plan is refused the same way, as `409 number_released`. ## How do I fix error 200? ### If you go through Genuka WA 1. **Check the number's state.** ```bash title="GET /api/v1/connections" curl "https://wa.genuka.com/api/v1/connections?companyId=cmp_123" \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" ``` ```json title="Response (excerpt)" { "data": [ { "id": "con_1", "companyId": "cmp_123", "displayPhoneNumber": "+237 6 90 …", "qualityRating": "GREEN", "status": "disconnected" } ] } ``` 2. **If it is `disconnected`, find out why before asking for a reconnection.** The status means Meta reported the account as no longer shared with Genuka, or deleted, or that you released the number yourself. A customer who removed access goes through the same `/connect/{your-slug}` link again: access comes back on the same record, with messages, templates and statistics kept and no new slot used in your plan ([Connect a number](https://wa.genuka.com/en/docs/onboarding)). A released number needs a free slot again. A deleted account, or one disabled by Meta ([error 368](https://wa.genuka.com/en/docs/errors/368)), is not restored by reconnecting: the `account_update` event forwarded to your webhooks tells you which case applies. If it is `offboarded`, follow the [coexistence guide](https://wa.genuka.com/en/docs/guides/coexistence). 3. **If it still reads `connected`, contact Genuka support** with the response's `x-request-id` header and `meta.traceId`. The status reflects the latest events received from Meta: it does not prove that access is intact. ### If you call the Cloud API with your own token 1. **Assign the system user to both accounts.** In Meta Business Suite: portfolio settings, **Accounts > WhatsApp accounts**, pick the account, **People** tab, **+Add people**, then the system user and its access level. Repeat under **Accounts > Messaging accounts** ([Meta, Access tokens](https://developers.facebook.com/documentation/business-messaging/whatsapp/access-tokens#business-asset-access)). 2. **Check the token's permissions** in the [access token debugger](https://developers.facebook.com/tools/debug/accesstoken/): `whatsapp_business_management` and `whatsapp_business_messaging`, plus `business_management` if you manage portfolio assets. 3. **Do not rely on the API cascade.** Access granted through the API on the Messaging account also applies to the linked WhatsApp account, but Meta calls this behaviour interim, and Meta Business Suite does not do it. Assign both accounts explicitly (same page). ## How do I prevent error 200? * **Subscribe an endpoint to `account_update`.** A `PARTNER_REMOVED` warns you the moment a customer removes access, before your sends start failing. * **Tell your customers what removal means.** Removing Genuka from their Business Manager cuts the API on every number of that WhatsApp account. * **If you manage your own tokens**, Meta says an admin system user has access by default to every WhatsApp account owned by or shared with your portfolio; an employee system user has to be assigned account by account. ## Related error codes * [190](https://wa.genuka.com/en/docs/errors/190): the token has expired or was invalidated. * [10](https://wa.genuka.com/en/docs/errors/10): permission not granted or removed, including OTP templates for an unverified business. * [3](https://wa.genuka.com/en/docs/errors/3): capability or permission missing for this endpoint. * [0](https://wa.genuka.com/en/docs/errors/0): Meta could not authenticate the app user. ## FAQ ### Is error 200 related to HTTP status 200? Not at all. It is a Meta error code, returned in the `code` field of a failed response. Meta points this out in its own documentation. ### My customer removed Genuka by mistake: what should they do? Go through your connect link again. The number comes back on the same record, with its history, and without using an extra slot. ### Why does the error only hit some of my numbers? Because access is granted per WhatsApp account. The numbers of a customer who removed access fail; those of your other customers keep working. ### Should I retry? No. Until access is restored, every new call will produce the same error. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Access tokens, business asset access](https://developers.facebook.com/documentation/business-messaging/whatsapp/access-tokens#business-asset-access) * [Meta — Register a business phone number](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/registration) * [Meta — account\_update webhook](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/account_update) * [Meta — Onboarding WhatsApp Business app users (coexistence)](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users/) * [Meta — Graph API, Handling errors](https://developers.facebook.com/docs/graph-api/guides/error-handling) --- # WhatsApp error 4: app rate limit reached — fix URL: https://wa.genuka.com/en/docs/errors/4 Language: English > WhatsApp error 4 (API Too Many Calls): the app hit its Meta API call rate limit. How it differs from 80007 and 130429, and how to retry safely. WhatsApp error 4 means the application calling Meta's API has reached its call rate limit. It is a temporary throttle: nothing is broken, you need to wait and send fewer requests. It applies to the application as a whole, not to one number or one WhatsApp account, which is what sets it apart from errors 80007 and 130429. ## What does error 4 mean? Meta lists it among the Cloud API's throttling errors: > "The app has reached its API call rate limit." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#throttling-errors) The suggested fix: open the app in the App Dashboard, **Application Rate Limit** section, confirm the limit was reached, then try again later or reduce the frequency and number of requests. The Graph documentation calls this code "API Too Many Calls", a temporary throttling issue: "Wait and retry the operation, or examine your API request volume" ([Meta, Graph API error handling](https://developers.facebook.com/docs/graph-api/guides/error-handling)). Its rate limit page adds that code 4 means the app whose token is used in the request has reached its limit ([Meta, Rate limits](https://developers.facebook.com/docs/graph-api/overview/rate-limiting)). ### Four limits, four codes | Code | What is counted | Scope | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------- | | **4** | The application's API calls | The whole application | | [80007](https://wa.genuka.com/en/docs/errors/80007) | An app's calls on a WhatsApp account: 200 per hour by default, 5,000 on an active account with a registered number | One app and one WhatsApp account | | [130429](https://wa.genuka.com/en/docs/errors/130429) | Messages per second: 80 by default | One number | | [131056](https://wa.genuka.com/en/docs/errors/131056) | Messages to the same recipient: one every 6 seconds | One sender and recipient pair | The figures come from Meta's platform overview ([Meta, About the platform](https://developers.facebook.com/documentation/business-messaging/whatsapp/about-the-platform#rate-limits)). ## When does error 4 happen? * **A burst of calls that are not sends**: listing templates in a loop, re-reading every number's health on each dashboard load, uploading media back to back. * **Several services on the same app**: a campaign worker, a back office and a sync script sharing one token add up their calls. * **A retry loop with no delay**: each failure fires another call immediately, and the limit moves further away instead of closer. ### Where do you see it in Genuka WA? With Genuka WA, the Meta application making the call is Genuka's, as Tech Provider: the application limit does not depend on your volume alone. Genuka puts code 4 in the `retryable` class. | Channel | What you get | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | | `POST /api/v1/messages` | `400`, `"error": "send_retryable"`, `meta.code: 4`, `meta.retryable: true` | | Campaign | Each recipient gets up to three attempts in total (two retries), waiting 0.25 to 0.5 s then 0.5 to 1 s, before turning `failed` | | `POST /api/v1/templates` | `422`, `"error": "meta_rejected"`, `meta.retryable: true` if Meta answered with a 4xx; `502` if it answered 429 or 5xx | That `502` arrives without a JSON body: our CDN replaces the response with the single line `error code: 502` ([API reference](https://wa.genuka.com/en/docs/api)). Treat it as a transient failure, without looking for `meta` in it. Genuka also limits the bursts it could cause on your behalf: `GET /api/v1/numbers?refresh=true` refuses the call (`400 too_many_connections`) above 25 numbers, so narrow it with `?companyId=`; and a template refresh processes WhatsApp accounts five at a time. ## How do I fix error 4? 1. **Retry with a growing delay, only when `meta.retryable` is `true`** or on a `5xx` with no JSON body. A single send is not retried by Genuka WA on your behalf: your code has to wait, doubling the delay on each attempt and adding a little randomness so your workers do not all restart together. For a send, retry only when `meta.code` is present (4, 80007, 130429…). A timeout between Genuka and Meta is also marked `retryable`, with no `meta.code`, even though Meta may already have accepted the message; the API has no idempotency key, and sending it again can deliver it twice. 2. **Drop unnecessary read calls.** `GET /api/v1/numbers` without `refresh=true` returns the stored state, kept current by Meta's webhooks. Template statuses arrive by [webhook](https://wa.genuka.com/en/docs/webhooks); the `POST /api/v1/templates/sync` refresh is a repair path, and once an hour is plenty ([API reference](https://wa.genuka.com/en/docs/api#templates)). 3. **If the error lasts more than an hour**, contact Genuka support with the `x-request-id` header and `meta.traceId`: Graph limits are counted over a rolling hour. If you call the Cloud API with your own app, the App Dashboard shows the app's current Application Rate Limit usage and the number of rate-limited users ([Meta, Rate limits](https://developers.facebook.com/docs/graph-api/overview/rate-limiting)). **Node.js** ```ts const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export async function callGenuka(path: string, body: unknown, maxAttempts = 5) { for (let attempt = 1; ; attempt++) { const response = await fetch(`https://wa.genuka.com/api/v1${path}`, { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); // A 5xx arrives without JSON: our CDN replaces it with "error code: 502". const json = await response.json().catch(() => null); if (response.ok) return json.data; // A send is only retried on a Meta code (4, 80007, 130429…): after a timeout, Meta may // already have accepted the message, and sending it again would deliver it twice. const retryable = path === "/messages" ? json?.meta?.retryable === true && json.meta.code !== undefined : response.status >= 500 || json?.meta?.retryable === true; if (!retryable || attempt >= maxAttempts) { throw new Error(`${response.status} ${json?.error ?? "no JSON body"} (Meta ${json?.meta?.code ?? "-"})`); } const base = Math.min(30_000, 1_000 * 2 ** (attempt - 1)); await sleep(base / 2 + Math.random() * (base / 2)); } } ``` **Python** ```python import os import random import time import requests def call_genuka(path: str, body: dict, max_attempts: int = 5) -> dict: for attempt in range(1, max_attempts + 1): response = requests.post( f"https://wa.genuka.com/api/v1{path}", headers={"Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}"}, json=body, timeout=30, ) try: payload = response.json() except ValueError: # A 5xx arrives without JSON: our CDN replaces it with "error code: 502". payload = {} if response.ok: return payload["data"] meta = payload.get("meta") or {} # A send is only retried on a Meta code (4, 80007, 130429…): after a timeout, Meta may # already have accepted the message, and sending it again would deliver it twice. if path == "/messages": retryable = bool(meta.get("retryable")) and meta.get("code") is not None else: retryable = response.status_code >= 500 or bool(meta.get("retryable")) if not retryable or attempt == max_attempts: raise RuntimeError(f"{response.status_code} {payload.get('error', 'no JSON body')} (Meta {meta.get('code')})") base = min(30.0, 2 ** (attempt - 1)) time.sleep(base / 2 + random.random() * base / 2) ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_USERAGENT => "acme-crm/1.0 (+https://example.com)", CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("GENUKA_WA_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode($body), ]); $raw = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); // 0 when the request never completed curl_close($ch); // A 5xx arrives without JSON: our CDN replaces it with "error code: 502". $json = is_string($raw) ? json_decode($raw, true) : null; if (!is_array($json)) $json = []; if ($status >= 200 && $status < 300) return $json["data"]; // A send is only retried on a Meta code (4, 80007, 130429…): after a timeout, Meta may // already have accepted the message, and sending it again would deliver it twice. $meta = $json["meta"] ?? []; $retryable = $path === "/messages" ? !empty($meta["retryable"]) && isset($meta["code"]) : $status === 0 || $status >= 500 || !empty($meta["retryable"]); if (!$retryable || $attempt >= $maxAttempts) { throw new RuntimeException("$status " . ($json["error"] ?? "no JSON body") . " (Meta " . ($meta["code"] ?? "-") . ")"); } $base = min(30.0, 2 ** ($attempt - 1)); usleep((int) (($base / 2 + mt_rand() / mt_getrandmax() * $base / 2) * 1_000_000)); } } ``` **curl** ```bash # A single attempt: read meta.retryable before deciding to try again. curl -s -X POST https://wa.genuka.com/api/v1/messages \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "to": "+237690000001", "text": "Your parcel is on its way." }' \ | jq '{error, code: .meta.code, retryable: .meta.retryable}' ``` ## How do I prevent error 4? * **Prefer webhooks to polling.** Message statuses, template reviews and quality changes arrive on their own; asking for them in a loop burns the limit for nothing. * **Send bulk through a campaign.** Genuka paces the send to the number's throughput and handles retries recipient by recipient — see [campaigns over the API](https://wa.genuka.com/en/docs/guides/campaigns-api). * **Never retry at a fixed interval.** Three attempts one second apart replay the same burst at the same moment. ## Related error codes * [80007](https://wa.genuka.com/en/docs/errors/80007): an app's call limit on one WhatsApp account. * [130429](https://wa.genuka.com/en/docs/errors/130429): a number's messages-per-second throughput. * [131056](https://wa.genuka.com/en/docs/errors/131056): too many messages to the same recipient in a short time. ## FAQ ### Does error 4 come from my Genuka API key? No. It is a Meta code, relayed in `meta.code`. It concerns the Meta application making the call, Genuka's, not your key. ### How long should I wait? Meta gives no fixed duration for code 4: Graph limits are computed over a rolling hour. Start with a growing delay, from one to thirty seconds, and cut down your calls; if the error lasts more than an hour, tell support. ### Is a message refused with code 4 billed? Meta did not accept it, so there is no message to bill. On the Genuka WA side, only messages Meta accepted count against your plan's quota. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Graph API, Handling errors](https://developers.facebook.com/docs/graph-api/guides/error-handling) * [Meta — Graph API, Rate limits](https://developers.facebook.com/docs/graph-api/overview/rate-limiting) * [Meta — About the platform, rate limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/about-the-platform#rate-limits) --- # WhatsApp error 80007: account rate limit reached — fix URL: https://wa.genuka.com/en/docs/errors/80007 Language: English > WhatsApp error 80007: the WhatsApp Business Account hit its API call limit (200 or 5,000 per hour). Which calls count, and how to avoid it. WhatsApp error 80007 means an application made too many calls on one WhatsApp Business account within the hour: 200 by default, 5,000 for an active account with a registered number. The calls that count are account management calls, such as templates and phone numbers, not message sends. Wait, then call less often. ## What does error 80007 mean? Meta lists it among the throttling errors: > "The WhatsApp Business Account has reached its rate limit." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#throttling-errors) Suggested fix: see the WhatsApp Business account rate limits, then "try again later or reduce the frequency or amount of API queries the app is making". Those limits are described in the platform overview: an app's requests on an account are counted over a rolling hour, at 200 per hour, per app, per account by default, and 5,000 per hour for an active account with at least one registered phone number ([Meta, About the platform](https://developers.facebook.com/documentation/business-messaging/whatsapp/about-the-platform#rate-limits)). They cover these endpoints: | Request | Endpoint | | ----------------------- | ------------------------------------- | | `GET` | `/{WABA_ID}` | | `GET`, `POST`, `DELETE` | `/{WABA_ID}/assigned_users` | | `GET` | `/{WABA_ID}/phone_numbers` | | `GET`, `POST`, `DELETE` | `/{WABA_ID}/message_templates` | | `GET`, `POST`, `DELETE` | `/{WABA_ID}/subscribed_apps` | | `GET` | `/{WABA_TO_NUMBER_CURRENT_STATUS_ID}` | Graph's general rate limit page also ties code `80008` to the "WhatsApp Business Management API" limit type. Handle both the same way ([Meta, Rate limits](https://developers.facebook.com/docs/graph-api/overview/rate-limiting)). ## When does error 80007 happen? * **Polling templates.** Re-reading the template list every minute to see whether one was approved costs 60 calls an hour on that account, before anything else. * **Bulk template creation.** A script that submits, deletes and recreates dozens of templates back to back. * **An account that is not active yet.** Until a number is registered on it, the account stays at 200 calls per hour. ### Where do you see it in Genuka WA? The limit is counted per application **and** per WhatsApp account. With Genuka WA the application is Genuka's: every call made on a customer's account counts together, whether it comes from your code, the dashboard or the client portal. On the other hand, one customer's account never uses up another customer's limit. On the Genuka WA side, these calls go through: | Genuka WA endpoint | Meta call counted | | ---------------------------------------------------------------- | --------------------------------------------------------------------- | | `POST /api/v1/templates` | `POST /{WABA_ID}/message_templates` | | `DELETE /api/v1/templates/{id}` | `DELETE /{WABA_ID}/message_templates` | | `POST /api/v1/templates/sync`, `GET /api/v1/templates?sync=true` | `GET /{WABA_ID}/message_templates`, once per WhatsApp account covered | Genuka puts code 80007 in the `retryable` class. A refused template creation comes back as `422 meta_rejected` with `meta.retryable: true`, or as a `502` if Meta answered with a 429 or a 5xx. A refresh stays a `200` as long as at least one account answered: the limited account shows `"ok": false` with Meta's reason in `error`. If every account refused, the response is a `502 meta_rejected`. A `502` reaches you without a JSON body: our CDN replaces it with the single line `error code: 502` ([API reference](https://wa.genuka.com/en/docs/api)). Read the HTTP status before the body, and treat this case as a transient failure. ## How do I fix error 80007? 1. **Stop asking for what arrives on its own.** A template review arrives by webhook, as a `message_template_status_update` event ([Webhooks](https://wa.genuka.com/en/docs/webhooks)). Subscribe an endpoint to it and remove the read loop. 2. **Keep the refresh for repairs.** `POST /api/v1/templates/sync` exists to catch up on a missed webhook: once an hour is plenty, and `companyId` or `connectionId` narrows it to the one account involved ([API reference](https://wa.genuka.com/en/docs/api#templates)). 3. **Retry what was refused, later.** A template creation marked `retryable` can be submitted again after a delay; the limit frees up as the rolling hour moves on. 4. **Spread bulk creation out.** Submit a new customer's templates in small batches rather than in one burst. If you call Graph yourself, the `X-Business-Use-Case-Usage` header gives the share of calls used (`call_count`) and, once throttled, `estimated_time_to_regain_access`, the number of minutes before things return to normal ([Meta, Rate limits](https://developers.facebook.com/docs/graph-api/overview/rate-limiting)). **Node.js** ```ts // One refresh an hour, narrowed to one customer, reporting the accounts that failed. const response = await fetch("https://wa.genuka.com/api/v1/templates/sync", { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ companyId: "cmp_1" }), }); // A 502 arrives without JSON: our CDN replaces it with "error code: 502". const json = await response.json().catch(() => null); if (!response.ok) { console.error("No account answered", response.status, json?.message ?? ""); } else { for (const waba of json.data.wabas.filter((w: { ok: boolean }) => !w.ok)) { console.warn("Account not refreshed, retry next hour", waba.wabaId, waba.error); } } ``` **Python** ```python import os import requests response = requests.post( "https://wa.genuka.com/api/v1/templates/sync", headers={"Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}"}, json={"companyId": "cmp_1"}, timeout=60, ) try: body = response.json() except ValueError: body = {} # a 502 arrives without JSON: our CDN replaces it with "error code: 502" if not response.ok: print("No account answered", response.status_code, body.get("message")) else: for waba in body["data"]["wabas"]: if not waba["ok"]: print("Account not refreshed, retry next hour", waba["wabaId"], waba.get("error")) ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_USERAGENT => "acme-crm/1.0 (+https://example.com)", CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("GENUKA_WA_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode(["companyId" => "cmp_1"]), ]); $raw = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); // 0 when the request never completed curl_close($ch); // A 502 arrives without JSON: our CDN replaces it with "error code: 502". $body = is_string($raw) ? json_decode($raw, true) : null; if ($status === 0 || $status >= 400 || !is_array($body)) { error_log("No account answered: HTTP $status " . ($body["message"] ?? "")); } else { foreach ($body["data"]["wabas"] as $waba) { if (!$waba["ok"]) error_log("Account not refreshed: {$waba['wabaId']} " . ($waba["error"] ?? "")); } } ``` **curl** ```bash # A 502 arrives as plain text ("error code: 502"), which jq then fails to read. curl -s -X POST https://wa.genuka.com/api/v1/templates/sync \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "companyId": "cmp_1" }' \ | jq '.data.wabas[] | select(.ok == false)' ``` ## How do I prevent error 80007? * **Webhooks first.** A template status, a quality change or an account update reaches you without you having to ask. * **A scheduled refresh, not one triggered by page views.** A dashboard that re-reads Meta every time it opens multiplies calls by the number of visitors. * **Connect the number before creating templates.** An active account with a registered number gets 5,000 calls per hour instead of 200. ## Related error codes * [4](https://wa.genuka.com/en/docs/errors/4): the call limit of the whole application. * [130429](https://wa.genuka.com/en/docs/errors/130429): a number's message throughput. * `80008`: the code Graph's rate limit page ties to the WhatsApp Business Management API. ## FAQ ### Why is my account limited to 200 calls per hour instead of 5,000? Meta reserves 5,000 calls per hour for active accounts with at least one registered number. An account created without a number, or whose number is not registered yet, stays at 200. ### Do message sends count against this limit? The endpoints Meta lists for this limit are account management ones, not sends. Sends have their own caps, including per-number throughput, code [130429](https://wa.genuka.com/en/docs/errors/130429). ### Can one customer's account exhaust another's limit? No. The limit is counted per application and per WhatsApp account: each account has its own. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — About the platform, rate limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/about-the-platform#rate-limits) * [Meta — Graph API, Rate limits](https://developers.facebook.com/docs/graph-api/overview/rate-limiting) --- # WhatsApp error 130429: Rate limit hit — cause and fix URL: https://wa.genuka.com/en/docs/errors/130429 Language: English > WhatsApp error 130429 (Rate limit hit) means your number exceeded its Cloud API throughput. The per-number caps, and how to retry without losing messages. WhatsApp error 130429 means your number went over its sending throughput on Meta's Cloud API: 80 messages per second by default, 20 for a number in coexistence with the WhatsApp Business app. Nothing is broken. Slow down, retry with a growing delay, and spread large sends over time instead of firing them all at once. ## What does error 130429 mean? Meta files it under throttling errors: > "Cloud API message throughput has been reached." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#throttling-errors) In other words, the number has used up its allowed message rate. In the Graph response, the `message` field combines the code and its title: `(#130429) Rate limit hit`. Meta warns that these titles will eventually be deprecated, so build your handling on the code, never on the text. Throughput is counted **per number** and includes inbound and outbound messages of every type ([Meta, Throughput](https://developers.facebook.com/documentation/business-messaging/whatsapp/throughput)): | Number | Maximum throughput | | ----------------------------------------------------------- | ---------------------- | | Standard Cloud API number | 80 messages/s | | Number automatically upgraded by Meta | up to 1,000 messages/s | | Coexistence number (also used in the WhatsApp Business app) | 20 messages/s, fixed | Meta keeps returning 130429 until you are back under that ceiling. ## When does error 130429 happen? * **A send loop with no pacing**: a script walking a 10,000-contact list as fast as the network allows crosses 80 messages/s within moments. * **A coexistence number**: the 20 messages/s cap applies whatever your tier. A 100,000-recipient campaign takes at least 83 minutes on it. * **Several senders on one number**: two workers, two campaigns, or a stream of API calls running next to a campaign all share the same throughput without coordinating. * **A spike of inbound messages**: they count against the same throughput as your sends. Do not confuse 130429 with two other ceilings: | Ceiling | What it counts | Code | | --------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | Throughput | Messages per second, per number | `130429` | | Pair rate limit | Messages to **the same** recipient | [`131056`](https://wa.genuka.com/en/docs/errors/131056) | | Messaging limit | Unique recipients outside the service window per rolling 24 h, at business portfolio level | see [Meta, Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits) | ### Where do you see it in Genuka WA? | Channel | What you get | | -------------------------------- | ------------------------------------------------------------------------------- | | `POST /api/v1/messages` response | `400`, `"error": "send_retryable"`, `meta.code: 130429`, `meta.retryable: true` | | Campaign | Each recipient is retried; it only turns `failed` after three attempts | | Dashboard, Logs section | The refused request, with the response body | ```json title="400 — POST /api/v1/messages response" { "error": "send_retryable", "message": "Cloud API message throughput has been reached.", "meta": { "errorClass": "retryable", "retryable": true, "code": 130429, "details": "Cloud API message throughput has been reached.", "traceId": "Az8or2yhqkZfEZ-_4Qn_Bam" } } ``` The HTTP status is `400`, not `429`: decide whether to retry from `meta.retryable`, not from the status. ## How do I fix error 130429? 1. **Retry with a growing delay.** A single send through `POST /api/v1/messages` is never retried by Genuka WA on your behalf: an interactive request should not take several seconds to fail quietly. Your code retries, with a wait that doubles on each attempt plus some jitter so your workers do not all restart in the same millisecond. 2. **Measure your real ceiling.** `GET /api/v1/numbers` returns, for each number, a `messagesPerSecond` field (20, 80 or 1,000) and `coexistence: true` when the number is also used in the app. Pace your sends on that value, not on a default of 80. 3. **Send in bulk through a campaign.** `POST /api/v1/campaigns`, then `/api/v1/campaigns/{id}/launch`: Genuka WA paces the run at the number's throughput and makes up to three attempts per recipient (waits of at most 500 ms, then 1 s, with jitter). A one-off 130429 does not cost you the recipient. See the [campaigns API guide](https://wa.genuka.com/en/docs/guides/campaigns-api). **Node.js** ```ts const API = "https://wa.genuka.com/api/v1"; export async function sendWithBackoff(payload: unknown, maxAttempts = 5) { for (let attempt = 1; ; attempt++) { const response = await fetch(`${API}/messages`, { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); // A 5xx can arrive as plain text (the proxy's "error code: 502"): JSON is not guaranteed. const json = await response.json().catch(() => null); if (response.ok) return json?.data; // { messageId, warning? } // meta.retryable is true for throughput limits (130429, 131056, 4, 80007), Meta internal // errors (131000, 5xx), stale media and 131048. That last one is retried later, not in this // loop (see the 131048 page). A 5xx with no `meta` does not tell whether the message left: // it is thrown as-is, check before resending to avoid a duplicate. const retryable = json?.meta?.retryable === true && json.meta.code !== 131048; if (!retryable || attempt >= maxAttempts) { throw new Error(`${response.status} ${json?.error ?? ""}: ${json?.message ?? ""}`); } const base = Math.min(30_000, 1_000 * 2 ** (attempt - 1)); await new Promise((resolve) => setTimeout(resolve, base / 2 + Math.random() * (base / 2))); } } ``` **Python** ```python import os import random import time import requests API = "https://wa.genuka.com/api/v1" HEADERS = { "Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}", "User-Agent": "acme-crm/1.0 (+https://example.com)", } def send_with_backoff(payload, max_attempts=5): for attempt in range(1, max_attempts + 1): response = requests.post(f"{API}/messages", json=payload, headers=HEADERS, timeout=30) try: body = response.json() except ValueError: # a proxy 5xx can arrive as plain text body = {} if response.ok: return body["data"] # {"messageId": ..., "warning": ...} meta = body.get("meta", {}) # 131048 is retryable, but later: see the 131048 page. A 5xx with no `meta` does not # tell whether the message left: it is raised as-is, check before resending. retryable = meta.get("retryable", False) and meta.get("code") != 131048 if not retryable or attempt == max_attempts: raise RuntimeError(f"{response.status_code} {body.get('error')}: {body.get('message', '')}") base = min(30.0, 2 ** (attempt - 1)) time.sleep(base / 2 + random.random() * base / 2) ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_USERAGENT => "acme-crm/1.0 (+https://example.com)", CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("GENUKA_WA_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode($payload), ]); $raw = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); // A proxy 5xx can arrive as plain text: JSON is not guaranteed. $json = is_string($raw) ? json_decode($raw, true) : null; $json = is_array($json) ? $json : []; if ($status >= 200 && $status < 400) { return $json["data"]; } $meta = $json["meta"] ?? []; // 131048: later (see its page). A 5xx with no "meta" is thrown as-is: check before // resending, the message may have left. $retryable = ($meta["retryable"] ?? false) && ($meta["code"] ?? null) !== 131048; if (!$retryable || $attempt >= $maxAttempts) { throw new RuntimeException("{$status} " . ($json["error"] ?? "")); } $base = min(30.0, 2 ** ($attempt - 1)); usleep((int) (($base / 2 + mt_rand() / mt_getrandmax() * $base / 2) * 1_000_000)); } } ``` **curl** ```bash # Each number's throughput, to pace your sends on (stored values). # To read them again from Meta: ?refresh=true&companyId=..., 25 numbers at most per call. curl "https://wa.genuka.com/api/v1/numbers" \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" # { "data": [ { "id": "con_1", "messagesPerSecond": 80, "coexistence": false, ... } ] } ``` ## How do I prevent error 130429? * **One sender per number.** Route every send for a number through a single queue, paced under `messagesPerSecond`. That is what stops two processes from stepping on each other. * **Plan the duration of large sends.** At 20 messages/s, a coexistence number sends at most 72,000 messages an hour. Announce the duration instead of discovering it. * **Use media hosted by Meta** for fast sends: Meta recommends sending a media ID rather than a URL on your own servers to take advantage of higher throughput ([Meta, Throughput](https://developers.facebook.com/documentation/business-messaging/whatsapp/throughput)). With Genuka WA, `assetId` does exactly that. * **Do not retry on a fixed interval.** A "three tries, one second apart" loop sends the same burst at the same moment and gets another 130429. ## Related error codes * [131056](https://wa.genuka.com/en/docs/errors/131056): too many messages to **the same** recipient in a short time. * [131048](https://wa.genuka.com/en/docs/errors/131048): sends restricted after blocks or spam reports, a quality problem rather than a speed problem. * [4](https://wa.genuka.com/en/docs/errors/4) and [80007](https://wa.genuka.com/en/docs/errors/80007): the two other limits in the same family, which count **API calls** by the app and by the WhatsApp Business account, not messages sent. ## FAQ ### Can my number go up to 1,000 messages per second? Yes, automatically and at no cost, when three conditions are met: an unlimited messaging limit, at least 100,000 unique users messaged outside a service window within a rolling 24 hours, and a quality rating of at least medium. During the upgrade, which takes up to a minute, the number answers `131057` ([Meta, Throughput](https://developers.facebook.com/documentation/business-messaging/whatsapp/throughput)). A coexistence number stays at 20 messages/s. ### Does Meta bill a message refused with 130429? No. Meta only charges for a template message when it is delivered ([Meta, Pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing)), and a message refused with 130429 never left. On the Genuka WA side, a refused send does not consume your subscription quota either: only messages Meta accepted are counted. ### Why does Genuka WA answer 400 and not 429? Because every Meta refusal on a send comes back as `400`, except token, permission and number registration problems, which come back as `409`. Whether to retry is in `meta.retryable` and `meta.errorClass`, which exist for that purpose. ### Can 130429 come from the Genuka WA API itself? No. 130429 is a Meta code that Genuka WA relays as-is in `meta.code`: it is always about the sending number's throughput at Meta, not about your API key. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Throughput](https://developers.facebook.com/documentation/business-messaging/whatsapp/throughput) * [Meta — Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits) * [Meta — Pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing) * [Meta — About the platform, rate limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/about-the-platform#rate-limits) --- # WhatsApp error 131048: sends restricted — cause and fix URL: https://wa.genuka.com/en/docs/errors/131048 Language: English > WhatsApp error 131048: Meta restricts the number's sends after blocks or spam reports. How to read the quality rating and bring it back up. WhatsApp error 131048 means Meta is limiting how many messages your number can send, because too many earlier messages were blocked or reported as spam by their recipients. It is a quality problem, not a speed problem. Pause your campaigns, check the number's quality rating, then resume with recipients who opted in. ## What does error 131048 mean? > "Message failed to send because there are restrictions on how many messages can be sent from > this phone number. This may be because too many previous messages were blocked or flagged as > spam." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#throttling-errors) Meta points to three things: the quality status in WhatsApp Manager, template messaging limits, and template quality. What they share is **message quality** as Meta measures it ([Meta, Send messages](https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/send-messages#message-quality)): * it reflects how your messages were received **over the past seven days**, weighted by recency; * it combines user feedback signals: blocks, reports, mutes, archives, and the reasons people give when they block you; * on a high-traffic number, it can change within minutes. The number's rating, status and messaging limit are shown in WhatsApp Manager under **Account tools > Phone numbers**. ## When does error 131048 happen? * **After a campaign sent to a cold list**: contacts imported without consent, old customers who were not expecting you. * **When one template keeps collecting negative feedback**: its own rating drops, then the rating of the number sending it. * **When messages follow each other too often** to the same people. Meta lists "sending too many messages per day" among the practices to avoid. ### Where do you see it in Genuka WA? | Channel | What you get | | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `POST /api/v1/messages` response | `400`, `"error": "send_retryable"`, `meta.code: 131048` | | `message_status` webhook, when Meta reports the failure afterwards | `data.status: "failed"`, `data.errors[0].code: 131048` | | Campaign | When Meta refuses on the call, each affected recipient is attempted up to three times, then turns `failed`. Reported by webhook, it turns `failed` with no new attempt | | `GET /api/v1/numbers/{id}/health` | `qualityRating` (`GREEN`, `YELLOW`, `RED`) read live from Meta, plus `alerts` when it changed | | `phone_number_quality_update` webhook | The quality or tier change, pushed by Meta | | `GET /api/v1/templates?sync=true` | `qualityScore.score` (`GREEN`, `YELLOW`, `RED`, `UNKNOWN`) for each template created through Genuka WA, to find the one causing trouble | Genuka WA classifies 131048 as `retryable`: the restriction eventually lifts, and a later attempt can go through. Our advice: here `meta.retryable: true` means "later", not "now". A campaign's three automatic attempts follow each other within two seconds and usually fail too: launch nothing more on this number until its rating has recovered. ## How do I fix error 131048? 1. **Pause the number's marketing sends.** Launch no new campaign until the rating recovers: every badly received message weighs on the next seven days. 2. **Read the number's rating** with `GET /api/v1/numbers/{id}/health`, or in WhatsApp Manager. 3. **Find the template at fault**: `GET /api/v1/templates?sync=true` returns `qualityScore.score` for each template created through Genuka WA; for those created in WhatsApp Manager, read the rating there. Stop the ones at `RED` or `YELLOW`, and rewrite them before using them again. 4. **Clean your audience**: keep the contacts who opted in and who read, drop the others, and honor every opt-out. 5. **Resume gradually**, with small sends to your most engaged customers, checking the rating at each step. **curl** ```bash curl https://wa.genuka.com/api/v1/numbers/con_1/health \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" # { # "data": { "connectionId": "con_1", "qualityRating": "YELLOW", "messagesPerSecond": 80, ... }, # "alerts": [ { "kind": "quality_dropped", "severity": "warning", "from": "GREEN", "to": "YELLOW", "message": "..." } ] # } ``` **Node.js** ```ts // Run before every campaign: do not start on a number that is already degraded. export async function canLaunchCampaign(connectionId: string): Promise { const response = await fetch(`https://wa.genuka.com/api/v1/numbers/${connectionId}/health`, { headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}` }, }); const { data } = await response.json(); return data.qualityRating === "GREEN"; } ``` **Python** ```python import os import requests def can_launch_campaign(connection_id: str) -> bool: """Run before every campaign: do not start on a number that is already degraded.""" response = requests.get( f"https://wa.genuka.com/api/v1/numbers/{connection_id}/health", headers={ "Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}", "User-Agent": "acme-crm/1.0 (+https://example.com)", }, timeout=30, ) response.raise_for_status() return response.json()["data"]["qualityRating"] == "GREEN" ``` ## How do I prevent error 131048? Meta's guidelines for high-quality messages ([Meta, Send messages](https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/send-messages#message-quality)): * follow the WhatsApp Business Messaging Policy; * only message people who opted in to receive messages from you; * make messages personalized and useful; * avoid open-ended welcome or introductory messages; * avoid sending too many messages per day; * optimize content and length. On the integration side, wire up the `phone_number_quality_update` webhook and alert as soon as a number turns `YELLOW`: that is the moment to act, before `RED`. ## Related error codes * [132015](https://wa.genuka.com/en/docs/errors/132015): one specific template is paused for low quality. * [131049](https://wa.genuka.com/en/docs/errors/131049): one recipient reached their marketing message limit; your number is not the issue. * [130429](https://wa.genuka.com/en/docs/errors/130429): plain throughput overflow, unrelated to quality. ## FAQ ### How long does a 131048 restriction last? Meta publishes no duration. Since quality is computed over the past seven days, expect at least several days of well-received messages before it recovers. ### Can I work around it with another number on the same account? It is not a fix: messaging limits are calculated at business portfolio level and shared by all of its numbers ([Meta, Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits)), and the cause, badly received messages, would follow you. ### Are service messages affected? Meta describes a restriction on how many messages the number can send, without distinguishing categories. Prioritize replies to your customers and transactional messages while the rating recovers. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Send messages, message quality](https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/send-messages#message-quality) * [Meta — Template quality rating](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/template-quality) * [Meta — Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits) --- # WhatsApp error 131056: pair rate limit — cause and fix URL: https://wa.genuka.com/en/docs/errors/131056 Language: English > WhatsApp error 131056: too many messages to the same recipient in a short time. Meta's pair rate limit (1 message every 6 s) and how to retry it properly. WhatsApp error 131056 means you sent too many messages to the same recipient in too short a time: at a steady pace, Meta allows one message every 6 seconds per sender-recipient pair. Wait before writing to that person again; your sends to other numbers can carry on without any pause at all. ## What does error 131056 mean? > "Too many messages sent from the sender phone number to the same recipient phone number in a > short period of time." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#throttling-errors) Meta advises waiting and retrying if you mean to message that number, and reminds you that you can message a **different** number without waiting. The rule behind this code is the **pair rate limit** ([Meta, About the platform](https://developers.facebook.com/documentation/business-messaging/whatsapp/about-the-platform#rate-limits)): | Rule | Value | | ---------------------------- | ------------------------------------------------------------------------------------------------------- | | Steady pace to one recipient | 1 message every 6 seconds (0.17 messages/s) | | Equivalent | about 10 messages a minute, 600 an hour | | Allowed burst | up to 45 messages in 6 seconds, borrowed from future quota | | After a burst | wait as long as those messages would have taken at the normal pace: a burst of 20 needs about 2 minutes | | Retry recommended by Meta | after 4^X seconds, X starting at 0 and growing by 1 after each failure | ## When does error 131056 happen? * **A duplicate in a campaign list.** Genuka WA does not deduplicate a campaign's recipients: the same number listed twice gets two back-to-back sends. * **A bot that splits its answer** into five or six bubbles sent in a row. * **A "Resend code" button** with no minimum delay between two taps. * **A non-idempotent webhook handler** that replies to every redelivery of the same inbound message. Genuka WA webhooks can arrive twice: deduplicate on `X-Genuka-Delivery` or on the `wamid`. ### Where do you see it in Genuka WA? | Channel | What you get | | -------------------------------- | ------------------------------------------------------------------------------- | | `POST /api/v1/messages` response | `400`, `"error": "send_retryable"`, `meta.code: 131056`, `meta.retryable: true` | | Campaign | The recipient is retried, then turns `failed` if all three attempts fail | | Dashboard, Logs section | The refused request and the response body | One detail matters for campaigns: the launcher's three attempts follow each other within two seconds (waits of at most 500 ms, then 1 s), less than the 6 seconds of the pair rate limit. A duplicate in the list therefore usually ends up `failed`. The fix is to deduplicate upstream, not to stretch the retries. ## How do I fix error 131056? 1. **Stop writing to that recipient for now**, and carry on normally with the others. 2. **Retry on Meta's schedule**: 1 s, then 4 s, then 16 s, then 64 s. 3. **Space sends per recipient**: one queue per number, with at least 6 seconds between two messages. **Node.js** ```ts const API = "https://wa.genuka.com/api/v1"; const PAIR_GAP_MS = 6_000; const lastSentTo = new Map(); // move this to Redis if you run several workers async function send(payload: { connectionId: string; to: string } & Record) { const response = await fetch(`${API}/messages`, { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); return { response, json: await response.json() }; } export async function sendToRecipient(payload: { connectionId: string; to: string } & Record) { // 1. Respect the minimum gap to this recipient. const wait = (lastSentTo.get(payload.to) ?? 0) + PAIR_GAP_MS - Date.now(); if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait)); // 2. On 131056, retry after 4^X seconds: 1 s, 4 s, 16 s, 64 s, so five attempts in all. for (let x = 0; x <= 4; x++) { const { response, json } = await send(payload); lastSentTo.set(payload.to, Date.now()); if (response.ok) return json.data; if (json.meta?.code !== 131056) throw new Error(`${json.error}: ${json.message ?? ""}`); if (x === 4) break; // no wait after the last attempt await new Promise((resolve) => setTimeout(resolve, 4 ** x * 1_000)); } throw new Error("131056: recipient still rate-limited, reschedule"); } ``` **Python** ```python import os import time import requests API = "https://wa.genuka.com/api/v1" HEADERS = { "Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}", "User-Agent": "acme-crm/1.0 (+https://example.com)", } PAIR_GAP = 6.0 last_sent_to: dict[str, float] = {} # move this to Redis if you run several workers def send_to_recipient(payload: dict) -> dict: wait = last_sent_to.get(payload["to"], 0) + PAIR_GAP - time.time() if wait > 0: time.sleep(wait) for x in range(5): # five attempts, with 1 s, 4 s, 16 s, 64 s between them response = requests.post(f"{API}/messages", json=payload, headers=HEADERS, timeout=30) last_sent_to[payload["to"]] = time.time() body = response.json() if response.ok: return body["data"] if body.get("meta", {}).get("code") != 131056: raise RuntimeError(f"{body.get('error')}: {body.get('message', '')}") if x == 4: break # no wait after the last attempt time.sleep(4 ** x) raise RuntimeError("131056: recipient still rate-limited, reschedule") ``` ## How do I prevent error 131056? * **Deduplicate your lists before `POST /api/v1/campaigns`**, on the normalized number (digits only, with country code): `+237 690 00 00 01` and `237690000001` are the same recipient. * **Group your bubbles.** One message of up to 4,096 characters beats five short ones sent in a row. * **Put a minimum delay on code resends**, 30 to 60 seconds, which also protects your users from abuse. * **Make webhook handling idempotent**, so a redelivery never triggers a second reply. ## Related error codes * [130429](https://wa.genuka.com/en/docs/errors/130429): the number's **overall** throughput is reached, across all recipients. * [131048](https://wa.genuka.com/en/docs/errors/131048): sends restricted because of spam reports; insisting on one recipient does not help. ## FAQ ### Does error 131056 block my sends to other customers? No. Meta says so: you can keep messaging other numbers without waiting. Only the sender-recipient pair concerned is slowed down. ### How many messages can I send one customer per hour? At a steady pace, about 600, one every 6 seconds. A burst of 45 messages in 6 seconds is tolerated, but you pay for it afterwards with an equivalent wait. ### Should I retry on a fixed or growing delay? Growing. Meta recommends 4^X seconds, X starting at 0: 1 s, 4 s, 16 s, 64 s. A short fixed delay resends the request while the limit is still in force. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — About the platform, pair rate limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/about-the-platform#rate-limits) * [Meta — Throughput](https://developers.facebook.com/documentation/business-messaging/whatsapp/throughput) --- # WhatsApp error 368: account restricted — cause and fix URL: https://wa.genuka.com/en/docs/errors/368 Language: English > WhatsApp error 368: the WhatsApp Business Account is restricted or disabled for a Meta policy violation. Durations, requesting a review, prevention. WhatsApp error 368 means the WhatsApp Business account linked to the app has been restricted or disabled for violating a platform policy. It is neither a bug nor a rate limit: retrying will not help. The way out goes through Business Support Home, where the business sees the violation, how long it lasts, and can request a review. ## What does error 368 mean? Meta lists it among the Cloud API's integrity errors: > "The WhatsApp Business Account associated with the app has been restricted or disabled for > violating a platform policy." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#integrity-errors) The suggested fix points to Meta's policy enforcement document. In the general Graph documentation, code 368 is called "Temporarily blocked for policies violations" ([Meta, Graph API error handling](https://developers.facebook.com/docs/graph-api/guides/error-handling)). ### What restrictions does Meta apply? Meta starts with a warning. If the account keeps violating the rules, restrictions grow longer step by step ([Meta, Policy enforcement](https://developers.facebook.com/documentation/business-messaging/whatsapp/policy-enforcement)): | Restriction | Effect | | --------------- | ----------------------------------------------------------------------- | | 1 or 3 days | No marketing, utility or authentication templates; no new phone numbers | | 5, 7 or 30 days | No messages at all; no new phone numbers | | Account lock | Indefinite block on all sends, lifted only through an appeal | | Disabled | Permanently removed from the platform, after several unheeded warnings | For severe harm, Meta names child exploitation, scams, terrorism and the sale of illegal drugs, the account can be offboarded immediately (same page). ## When does error 368 happen? Meta names spam, template misclassification, high-risk categories (adult content, alcohol and tobacco, drugs, gambling, unsafe supplements) and excessive negative user feedback as reasons for restrictions (same page). In practice: * **Messages to people who never asked for them**, who then block or report the number. * **A template classified `UTILITY` whose content is promotional.** Meta counts misclassification as a violation. * **An activity WhatsApp's commerce rules forbid or tightly restrict.** ### Where do you see it in Genuka WA? Genuka WA puts code 368 in the `config` class: the account can no longer do what is asked, and no retry will change that. | Channel | What you get | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `POST /api/v1/messages` | `409`, `"error": "send_config"`, `meta.code: 368` | | Campaign | Each refused recipient turns `failed` with no retry; for a `MARKETING` campaign, Genuka first tries the Marketing Messages API, then the `/messages` endpoint, which refuses the same way | | `account_update` webhook | Meta's event, forwarded as is in `data` | Meta documents the `account_update` events to watch ([account\_update webhook](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/account_update)): | `event` | What it announces | | --------------------- | ------------------------------------------------------------------------------------------------ | | `ACCOUNT_VIOLATION` | A violation, with its type in `violation_info` | | `ACCOUNT_RESTRICTION` | A restriction, with its type (for example `RESTRICTED_BIZ_INITIATED_MESSAGING`) and its end date | | `DISABLED_UPDATE` | The account's ban state: `SCHEDULE_FOR_DISABLE`, `DISABLE` or `REINSTATE` | Genuka forwards each event type only once per WhatsApp account until its event log purges it, according to your plan's retention period: a second `ACCOUNT_RESTRICTION` on the same account within that window may never reach you. To know the current restriction, rely on Business Support Home rather than on the webhook alone. ## How do I fix error 368? Neither Genuka nor your code can lift a restriction: the decision is Meta's, and only the business that owns the account can contest it. 1. **Pause sends on that account.** Suspend the campaigns and automations that target its numbers: while the restriction lasts, every refused send only adds to the list of failures to replay. 2. **Open Business Support Home.** In Meta Business Suite: **All tools**, then **Business Support Home**, then **Account Overview**. The violation is described there: the policy violated, examples of allowed and disallowed content, active restrictions, what happens on a repeat, and how to appeal (same Meta page). 3. **Request a review if you are compliant.** Select the account, pick the violation, **Request Review**, add your supporting details and submit. Meta says the decision typically takes 24 to 48 hours, either "Unchanged" or "Reversed". Not every spam violation can be appealed: sometimes you have to wait for the restriction to end. 4. **Fix the cause before resuming.** Contact list, template classification, send frequency: resume with small volumes to contacts who have opted in. ## How do I prevent error 368? * **Only message people who agreed to it**, and give them a simple way to stop receiving your marketing messages. * **Classify your templates honestly.** A promotion is not a utility message. * **Watch quality before the penalty.** Subscribe an endpoint to `account_update` and `phone_number_quality_update` ([Webhooks](https://wa.genuka.com/en/docs/webhooks)): falling quality shows up before a restriction does. * **Keep an eye on [131048](https://wa.genuka.com/en/docs/errors/131048)**: sends limited after blocks or reports are often the early warning. ## Related error codes * `131031`: account restricted or disabled, or request data that does not match the account (a wrong two-step verification PIN, for instance). * [131048](https://wa.genuka.com/en/docs/errors/131048): sends limited after too many blocked or reported messages. * `130497`: account barred from messaging users in certain countries. * `131064`: messaging limit imposed after template classification violations. ## FAQ ### How long does a restriction last? It depends on severity and repetition: 1 or 3 days for templates, 5, 7 or 30 days for all sends, and an open-ended lock until an appeal succeeds. The `ACCOUNT_RESTRICTION` event carries the end date when there is one; if it was not forwarded to you, Business Support Home lists the active restrictions. ### Can Genuka lift the restriction? No. The decision and the review belong to Meta. Genuka relays the error and the account events; the appeal is made from the business's Business Support Home. ### Are my other customers affected? No. The restriction applies to one WhatsApp Business account. Your other customers' accounts are not concerned. ### Should I retry the refused sends? Not during the restriction. Resume after its end date, or after a "Reversed" decision. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Policy enforcement](https://developers.facebook.com/documentation/business-messaging/whatsapp/policy-enforcement) * [Meta — account\_update webhook](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/account_update) * [Meta — Graph API, Handling errors](https://developers.facebook.com/docs/graph-api/guides/error-handling) --- # WhatsApp error 141010: business not verified — fix URL: https://wa.genuka.com/en/docs/errors/141010 Language: English > WhatsApp error 141010 "The Business has not passed business verification": why your OTP templates are refused, and how to unblock them. WhatsApp error 141010 means the business that owns the WhatsApp account has not passed Meta business verification. It shows up in the account's health status, not in a send response. The practical consequence: authentication templates (OTP codes) are refused, while marketing and utility templates keep working as usual on the same account. ## What does error 141010 mean? You will not find it in Meta's public list of Cloud API error codes. It comes from the `health_status` field Meta exposes on the WhatsApp account, the phone number and templates. For each entity involved (number, account, business portfolio, app), Meta can attach an `errors` list where each item carries an `error_code`, an `error_description` and a `possible_solution` ([Meta, Health status](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/health-status)). On the accounts connected to Genuka WA whose business is not verified, this is what we read: ```json title="Excerpt of health_status" { "error_code": 141010, "error_description": "The Business has not passed business verification" } ``` Business verification applies to the Meta business portfolio that owns the WhatsApp account. It has nothing to do with Genuka's provider status: Genuka is a Meta Tech Provider, and it is your business that needs to be verified. ## When does error 141010 happen? As soon as a number is connected under an unverified portfolio. Meta's Embedded Signup lets you connect a number without verification, and many businesses start that way. The code stays silent until the day you create an authentication template: | What you do | Unverified business | Verified business | | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | Create a `MARKETING` or `UTILITY` template | Accepted | Accepted | | Create an `AUTHENTICATION` (OTP) template | Refused: error [10](https://wa.genuka.com/en/docs/errors/10), "Application does not have permission for this action" | Accepted once the limit has moved to 2,000 | | Reach unique recipients outside the service window, per 24 hours | 250 to start | 2,000 once Meta approves your message quality, then automatic increases | The limits are Meta's: a new portfolio starts at 250, and verification is one of the paths to 2,000 ([Meta, Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits)). Why OTP codes? Meta reserves the `AUTHENTICATION` category for businesses that completed one of its scaling paths, such as business verification, with a messaging limit of at least 2,000 business-initiated conversations a day; both conditions must be met ([360dialog, Authentication messages](https://docs.360dialog.com/docs/resources/authentication-messages)). Verification alone is therefore not enough: after a scaling path, Meta analyzes your message quality and can deny the increase, in which case your limit stays where it is ([Meta, Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits)). Of 60 Genuka WA customer accounts audited in September 2026, none of the 45 unverified ones holds an authentication template; the only one in the set belongs to a verified account. ### Where do you see it in Genuka WA? | Channel | What you see | | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | | `POST /api/v1/templates` with `"category": "AUTHENTICATION"` | `403 meta_rejected`, `meta.code: 10`, message "Application does not have permission for this action" | | Meta Business Suite | The portfolio's verification status, under **Security Center** | | Genuka's MCP server, `create_template` tool | The error comes with an explanation of this case for your AI agent — see [AI agents](https://wa.genuka.com/en/docs/guides/ai-agents) | The Genuka WA API does not relay `health_status` itself: you have no Meta token to query it with, and the verification status is easier to read in Meta Business Suite. ## How do I fix error 141010? 1. **Check the current status.** In Meta Business Suite, open **Security Center > Business Verification** on the portfolio that owns the WhatsApp account ([Meta, Embedded Signup errors](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/errors)). If you call Graph yourself, `GET /{WABA_ID}?fields=business_verification_status,health_status` returns both; Meta documents values such as `VERIFIED`, `PENDING` and `NOT_VERIFIED` for the first ([Meta, WhatsApp Business Account API](https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/whatsapp-business-account-api)). Compare case-insensitively: the API returned `not_verified`, in lowercase, when we checked. 2. **Start verification.** You request it from Meta Business Suite, on that same portfolio ([Meta, Verify your business](https://www.facebook.com/business/help/2058515294227817)). Genuka plays no part in it: it is between your business and Meta. 3. **Recreate the authentication template.** Once the business is verified and the limit has moved to 2,000 (shown in WhatsApp Manager, **Account tools > Messaging limits**, or in the number's `whatsapp_business_manager_messaging_limit` field), submit the same template again. Meta writes the text of authentication templates itself: you only provide the structure. **Node.js** ```ts const response = await fetch("https://wa.genuka.com/api/v1/templates", { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ connectionId: "con_1", name: "verification_code", language: "en_US", category: "AUTHENTICATION", messageSendTtlSeconds: 600, components: [ { type: "BODY", add_security_recommendation: true }, { type: "FOOTER", code_expiration_minutes: 10 }, { type: "BUTTONS", buttons: [{ type: "OTP", otp_type: "COPY_CODE", text: "Copy code" }] }, ], }), }); const json = await response.json(); if (response.status === 403 && json.meta?.code === 10) { // Unverified business (141010): nothing to fix in your code or your key. throw new Error("Meta business verification required for OTP templates"); } if (!response.ok) throw new Error(`${response.status} ${json.error}: ${json.message ?? ""}`); console.log(json.data.id, json.data.status); ``` **Python** ```python import os import requests response = requests.post( "https://wa.genuka.com/api/v1/templates", headers={"Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}"}, json={ "connectionId": "con_1", "name": "verification_code", "language": "en_US", "category": "AUTHENTICATION", "messageSendTtlSeconds": 600, "components": [ {"type": "BODY", "add_security_recommendation": True}, {"type": "FOOTER", "code_expiration_minutes": 10}, {"type": "BUTTONS", "buttons": [{"type": "OTP", "otp_type": "COPY_CODE", "text": "Copy code"}]}, ], }, timeout=30, ) body = response.json() if response.status_code == 403 and (body.get("meta") or {}).get("code") == 10: # Unverified business (141010): nothing to fix in your code or your key. raise RuntimeError("Meta business verification required for OTP templates") response.raise_for_status() print(body["data"]["id"], body["data"]["status"]) ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_USERAGENT => "acme-crm/1.0 (+https://example.com)", CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("GENUKA_WA_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "connectionId" => "con_1", "name" => "verification_code", "language" => "en_US", "category" => "AUTHENTICATION", "messageSendTtlSeconds" => 600, "components" => [ ["type" => "BODY", "add_security_recommendation" => true], ["type" => "FOOTER", "code_expiration_minutes" => 10], ["type" => "BUTTONS", "buttons" => [["type" => "OTP", "otp_type" => "COPY_CODE", "text" => "Copy code"]]], ], ]), ]); $body = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($status === 403 && ($body["meta"]["code"] ?? null) === 10) { // Unverified business (141010): nothing to fix in your code or your key. throw new RuntimeException("Meta business verification required for OTP templates"); } echo $body["data"]["id"] ?? $body["error"]; ``` **curl** ```bash curl -X POST https://wa.genuka.com/api/v1/templates \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "name": "verification_code", "language": "en_US", "category": "AUTHENTICATION", "messageSendTtlSeconds": 600, "components": [ { "type": "BODY", "add_security_recommendation": true }, { "type": "FOOTER", "code_expiration_minutes": 10 }, { "type": "BUTTONS", "buttons": [ { "type": "OTP", "otp_type": "COPY_CODE", "text": "Copy code" } ] } ] }' # Unverified business: 403 { "error": "meta_rejected", "meta": { "code": 10, … } } ``` ### What if the business cannot be verified right away? Meta describes two other paths to 2,000 recipients a day: verification by the partner that onboarded you, and 2,000 delivered messages outside service windows to unique recipients over a rolling 30 days, using high-quality templates ([Meta, Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits)). They raise your messaging limit. According to 360dialog, the OTP condition is a completed scaling path, not verification as such; but none of the unverified accounts we audited holds an authentication template. In the meantime, send your codes by SMS or email and keep WhatsApp for your notifications: see [WhatsApp API without Meta verification](https://wa.genuka.com/en/docs/guides/without-meta-verification). ## How do I prevent error 141010? * **Start verification as soon as you plan OTP codes**, before writing the login flow: it is the one step you do not control. * **Verify the right portfolio.** The one that owns the connected WhatsApp account, not another portfolio of the same company. * **Test creating the `AUTHENTICATION` template on the production account**: an unverified test account will always fail, whatever your code does. ## Related error codes * [10](https://wa.genuka.com/en/docs/errors/10): the response Meta returns when the OTP template is created. * [200](https://wa.genuka.com/en/docs/errors/200): a genuine permission or account access problem, not to be confused with this one. * `2388098`: during Embedded Signup, Meta limits how many WhatsApp accounts an unverified business can create ([Meta, Embedded Signup errors](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/errors)). ## FAQ ### Can you send WhatsApp messages without business verification? Yes. Marketing and utility templates, and replies inside the 24-hour service window, work without verification, within 250 unique recipients per 24 hours to start. Only OTP codes require it. ### Why does the error talk about an application permission? Because Meta answers the template creation with a generic permission error, code 10. The real cause is the business: the same account creates marketing templates without any problem. ### Why is 141010 missing from Meta's list of error codes? It is a `health_status` diagnostic code, describing why an entity is limited, not a code returned by an API call. Meta's public list only covers the latter. ### Do I need to recreate the template after verification? Yes. A template refused at creation does not exist on Meta's side: submit it again once the business is verified and the account's limit has moved to 2,000. ## Sources * [Meta — Health status](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/health-status) * [Meta — Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits) * [Meta — WhatsApp Business Account API](https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/whatsapp-business-account-api) * [Meta — Embedded Signup flow errors](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/errors) * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Verify your business in Meta Business Suite](https://www.facebook.com/business/help/2058515294227817) * [360dialog — Authentication messages](https://docs.360dialog.com/docs/resources/authentication-messages) --- # WhatsApp error 33: phone number deleted — cause and fix URL: https://wa.genuka.com/en/docs/errors/33 Language: English > WhatsApp Cloud API error 33: the target WhatsApp Business number was deleted, or the ID is no longer the right one. How to diagnose and reconnect. WhatsApp error 33 means, according to Meta, that the WhatsApp Business phone number the request targets has been deleted. The phone number ID you are using no longer points to an active number. Check that ID; if the number really was deleted, it has to be connected again. Sending the same request again will change nothing. ## What does error 33 mean? Meta files it under the Cloud API's "other errors": > "The business phone number has been deleted." — suggested fix: "Verify that the business phone > number is correct." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#other-errors) Meta's fix therefore covers two causes: a number that really was deleted, or an ID that is not the right one. ### Deleted, deregistered or still being deleted? Three similar states produce three different codes: | Number state | Code returned | What to do | | ----------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------- | | Deleted from the WhatsApp account | **33** | Check the ID, then add and reconnect the number | | Deregistered from the Cloud API, but still present | `133010`: "Phone number not registered on the WhatsApp Business Platform" | Register it again | | Deleted a few minutes ago, and being registered again | `133015`: deletion has not completed | Wait 5 minutes before retrying | The wording comes from the same Meta page. Meta also states that deregistering a number does not delete it or its message history ([Meta, Registration](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/registration)). ## Error 33 or subcode 33? First check which field carries the 33. Graph can also answer with code `100` and `error_subcode: 33`, a response that says nothing about a deleted number: ```json title="Code 100, subcode 33" { "error": { "message": "Unsupported post request. Object with ID '123456789012345' does not exist, cannot be loaded due to missing permissions, or does not support this operation.", "type": "GraphMethodException", "code": 100, "error_subcode": 33 } } ``` This response was reported on the [Activepieces forum](https://community.activepieces.com/t/whatsapp-business-error/4324). It says the target object does not exist, or the token has no access to it. The reported causes are a wrong ID, a token that lost access to that asset, or partner access that was removed ([Fast2SMS](https://www.fast2sms.com/help/?p=17132)). A deleted number can produce it, and so can removed access: do not conclude the number was deleted before checking access, see [error 100](https://wa.genuka.com/en/docs/errors/100) and [error 200](https://wa.genuka.com/en/docs/errors/200). ## When does error 33 happen? * **The number was deleted in WhatsApp Manager** by someone at the business, while an integration was still using it. * **The ID is wrong or stale**: copied from another account, kept in a configuration after a number change, or confused with the displayed phone number. ### Where do you see it in Genuka WA? Genuka WA stores the number's ID when it is connected; the `connectionId` you pass points to it. If the business later deletes the number in WhatsApp Manager, calls on that connection are refused: with `meta.code: 33`, or with `meta.code: 100` and `meta.subcode: 33`, the response described above. Genuka does not put 33 in a dedicated class: the class follows the HTTP status Meta returned, usually `unknown`, and it is not retryable. | Channel | What you get | | ---------------------------------- | ------------------------------------------------------------------------------------- | | `POST /api/v1/messages` | `400 send_unknown` (or `409 send_config` if Meta answers 401 or 403), `meta.code: 33` | | `GET /api/v1/numbers/{id}/health` | `422 meta_error` (or `403` if Meta answers 401 or 403), with the same `meta` object | | `GET /api/v1/numbers?refresh=true` | The number comes back with `"refreshed": false` and a `refreshError` | | Campaign | Each recipient turns `failed`, with Meta's message | ## How do I fix error 33? ### If you go through Genuka WA 1. **Confirm the number no longer answers.** `GET /api/v1/numbers/{id}/health` reads Meta for that one number: a `meta_error` refusal whose `meta.code` or `meta.subcode` is 33 confirms it in a single call. Also read its status in `GET /api/v1/connections`: if it is no longer `connected`, access may have been removed rather than the number deleted, see [error 200](https://wa.genuka.com/en/docs/errors/200). 2. **Have the number checked in WhatsApp Manager**, **Phone numbers** tab, by someone with access to the business's WhatsApp account. If it is still listed, the stored ID is no longer right: contact Genuka support with `x-request-id` and `meta.traceId`. 3. **If it was deleted, reconnect it.** The business goes through the [connect link](https://wa.genuka.com/en/docs/onboarding) again, which adds the number through Meta's Embedded Signup and registers it on the Cloud API. Then read `GET /api/v1/connections` again and use the `connectionId` listed there for this number, rather than the one you had kept. If Meta gave the number a new identifier, reconnecting creates a new connection, which needs a free slot in your plan. If every slot is in use, first release the old one from the client's page in the dashboard (**Release the slot**): it no longer sends, but it still holds a slot, and the reconnection would be refused with `plan_limit_numbers`. If Meta gave the number a new identifier, the messages, templates and statistics Genuka WA already recorded stay attached to the old connection. ### If you call the Cloud API directly List the account's numbers with `GET /{WABA_ID}/phone_numbers` and compare their `id` with the one you use. If the number is gone, add it back in WhatsApp Manager, verify it, then register it with `POST /{PHONE_NUMBER_ID}/register` ([Meta, Registration](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/registration)). Right after a deletion, wait a few minutes: Meta answers `133015` until it has completed. ## How do I prevent error 33? * **Stop sends before deleting a number.** Tell the people who manage WhatsApp Manager: deleting a number immediately stops every integration that uses it. * **Reference the connection, not the phone number.** Store Genuka WA's `connectionId`, or Meta's phone number ID, rather than the displayed number, which is not an API identifier. * **Check health before a large campaign.** A `GET /api/v1/numbers/{id}/health` fails right away on a deleted number, instead of letting every recipient fail one by one. ## Related error codes * `133010`: the number exists but is not registered on the Cloud API. * `133015`: the number was just deleted and deletion has not completed. * [100](https://wa.genuka.com/en/docs/errors/100): an unknown or misspelled parameter; with subcode 33, an object that cannot be found or accessed. * [200](https://wa.genuka.com/en/docs/errors/200): the account is no longer accessible, for instance after access was removed. ## FAQ ### What is the difference between deregistering and deleting a number? Deregistering makes the number unusable with the Cloud API until it is registered again, without deleting it or its history. Deleting removes it from the WhatsApp account: that is the case that produces error 33. ### Should I retry a send refused with error 33? No. Until the number is reconnected, every send on that connection will fail the same way. ### Is my message history lost? Not on the Genuka WA side: what was recorded stays attached to the old connection. On Meta's side, yes: deleting a number also deletes its history, whereas deregistering it keeps it ([Meta, Registration](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/registration)). ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Register a business phone number](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/registration) * [Activepieces community — code 100, error\_subcode 33](https://community.activepieces.com/t/whatsapp-business-error/4324) * [Fast2SMS — Unsupported POST/GET request, object does not exist](https://www.fast2sms.com/help/?p=17132) --- # WhatsApp error 100: Invalid parameter — cause and fix URL: https://wa.genuka.com/en/docs/errors/100 Language: English > WhatsApp Cloud API error 100 (Invalid parameter): an unknown, misspelled or too-long parameter. How to read details and fix the request. WhatsApp error 100 means your Cloud API request contains a parameter Meta does not recognise, a misspelled one, or a value over a length limit. It is usually a content error: sending the same request again will always fail. The cause is generally spelled out in the `details` field; with subcode 33, the ID you are targeting is what is wrong. ## What does error 100 mean? Meta files it under the Cloud API's "other errors": > "The request included one or more unsupported or misspelled parameters." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#other-errors) Meta's suggested fix lists four checks: read the endpoint reference for the supported parameters and their spelling; for WhatsApp Flows with an endpoint, provide a valid 2048-bit RSA public key in PEM format; make sure the `phone_number_id` you register matches the one stored previously; and stay under each type's length restrictions. In the Graph response, `message` is often `(#100) Invalid parameter`, a generic title. The useful information is elsewhere: Meta describes `details` as the field that can say "which parameter is invalid or what values are acceptable" (same page). ## When does error 100 happen? | Situation | What Meta returns | Source | | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | Unknown or misspelled parameter | `(#100) Invalid parameter`, with the offending parameter in `details` | Meta | | Value over a length limit | Same code | Meta | | Flows public key that is not a 2048-bit RSA key in PEM | Same code | Meta | | Non-template message sent to the Marketing Messages API | `details`: "Message must be a template message." | [Meta](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#marketing-messages-api-for-whatsapp-error-codes) | | Wrong phone number or account ID, or a token without access to that asset | `error_subcode: 33` and "Unsupported post request. Object with ID '…' does not exist, cannot be loaded due to missing permissions, or does not support this operation", with no `details` | Developer reports: [Activepieces](https://community.activepieces.com/t/whatsapp-business-error/4324), [Fast2SMS](https://www.fast2sms.com/help/?p=17132) | | Sending from a number to itself | `(#100) Invalid parameter`, with no detail at all | Genuka observation | | Deleting a template without rights on the WhatsApp account | `(#100) Need permission on either WhatsApp Business Account or owner/shared business` | Genuka observation | The last two rows come from our own calls. For a send to oneself, Meta actually documents a dedicated code, `131021` ("Sender and recipient phone number is the same"), but what we received was a bare `100`. ### Where do you see it in Genuka WA? Genuka WA validates much of the request **before** Meta, and then answers without Meta seeing anything: | Genuka check | Response | | ---------------------------------------------------------------------- | ------------------------------------------------------------------- | | Message with no content field, media with no `assetId`, `id` or `link` | `400 missing_content`, `400 invalid_media` | | Template with malformed components | `400 invalid_template` (can be turned off with `"validate": false`) | | `to` equal to the sending number | `400 recipient_is_sender` | When the refusal comes from Meta, Genuka relays code 100 without reclassifying it: the class is `unknown`, not retryable. ```json title="400 — POST /api/v1/messages" { "error": "send_unknown", "message": "…the details text, when Meta provides one…", "meta": { "errorClass": "unknown", "retryable": false, "code": 100, "details": "…", "traceId": "AbC…" } } ``` On `POST /api/v1/templates`, the same refusal comes back as `422 meta_rejected` with the same `meta` object. The `raw` field of `POST /api/v1/messages` is never validated by Genuka: errors come straight back from Meta ([Send messages](https://wa.genuka.com/en/docs/messages)). In a `MARKETING` campaign, a `100` from the Marketing Messages API makes Genuka resend the message through the regular `/messages` endpoint; if that refuses it too, the recipient turns `failed`. ### What about subcode 33? It is not a content error. With `meta.code: 100` and `meta.subcode: 33`, and a message starting with "Unsupported post request" (or "get" for a read), Graph is saying the target object does not exist, or the token has no access to it. The reported causes are a wrong phone number or WhatsApp account ID, a token that lost access to that asset, or partner access that was removed ([Fast2SMS](https://www.fast2sms.com/help/?p=17132)). With Genuka WA, read `GET /api/v1/connections`: a number whose status is no longer `connected` belongs on the [error 200](https://wa.genuka.com/en/docs/errors/200) page; a number deleted on Meta's side, on the [error 33](https://wa.genuka.com/en/docs/errors/33) page. ## How do I fix error 100? 1. **Read `meta.details`.** It is the only part of the response that names the problem. Log it with `meta.traceId` and the `x-request-id` header. 2. **Compare the request with the reference.** For sends, the [API reference](https://wa.genuka.com/en/docs/api#messages) and [Send messages](https://wa.genuka.com/en/docs/messages) list every accepted field and its limits: 4,096 characters for a text, 1,024 for an image caption, 20 for a button title, at most 3 reply buttons. 3. **Remove what you added "just in case".** A Cloud API field copied into `raw` but unknown to the endpoint is enough to trigger a `100`. 4. **If `details` is empty, shrink the request.** Send the same message stripped down (a plain `text` to the same number), then add elements back one at a time. If a minimal message fails too, check that `to` is not the sending number and contact Genuka support with `x-request-id` and `meta.traceId`. **Node.js** ```ts const response = await fetch("https://wa.genuka.com/api/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ connectionId: "con_1", to: "+237690000001", text: "Your parcel is on its way." }), }); const json = await response.json(); if (!response.ok) { // Everything needed to diagnose, without sending the request again. console.error({ status: response.status, error: json.error, metaCode: json.meta?.code, details: json.meta?.details ?? json.message, traceId: json.meta?.traceId, requestId: response.headers.get("x-request-id"), }); } ``` **Python** ```python import os import requests response = requests.post( "https://wa.genuka.com/api/v1/messages", headers={"Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}"}, json={"connectionId": "con_1", "to": "+237690000001", "text": "Your parcel is on its way."}, timeout=30, ) if not response.ok: body = response.json() meta = body.get("meta") or {} print({ "status": response.status_code, "error": body.get("error"), "meta_code": meta.get("code"), "details": meta.get("details") or body.get("message"), "trace_id": meta.get("traceId"), "request_id": response.headers.get("x-request-id"), }) ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_USERAGENT => "acme-crm/1.0 (+https://example.com)", CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("GENUKA_WA_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "connectionId" => "con_1", "to" => "+237690000001", "text" => "Your parcel is on its way.", ]), ]); $body = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($status >= 400) { error_log(json_encode([ "status" => $status, "error" => $body["error"] ?? null, "meta_code" => $body["meta"]["code"] ?? null, "details" => $body["meta"]["details"] ?? ($body["message"] ?? null), "trace_id" => $body["meta"]["traceId"] ?? null, ])); } ``` **curl** ```bash curl -s -X POST https://wa.genuka.com/api/v1/messages \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "to": "+237690000001", "text": "Your parcel is on its way." }' \ | jq '{error, code: .meta.code, details: .meta.details, traceId: .meta.traceId}' ``` ## How do I prevent error 100? * **Keep Genuka's validation on.** On `POST /api/v1/templates`, only pass `"validate": false` for a component Meta accepts and our validator refuses. * **Prefer typed fields over `raw`.** Typed fields are checked for shape before sending (required fields, media with no `id` or link); length and count limits are still enforced by Meta, and `raw` is not checked at all. * **Test each new message type on a test number** before wiring it into a bulk send: a `100` is the same for every recipient. ## Related error codes * `131008`: a required parameter is missing. * `131009`: the parameter exists, but its value is invalid. * `131021`: sender and recipient are the same number. * [132000](https://wa.genuka.com/en/docs/errors/132000): the number of variables sent does not match the template. * [33](https://wa.genuka.com/en/docs/errors/33): number deleted (code 33), not to be confused with subcode 33 of code 100, which flags an object that cannot be found or accessed (deleted number or lost access). * [200](https://wa.genuka.com/en/docs/errors/200): access to the WhatsApp account was removed, or a permission is missing. ## FAQ ### Why is `details` sometimes empty? Meta does not always fill it. Sending from a number to itself, for instance, comes back as `(#100) Invalid parameter` with nothing more, which is why Genuka WA checks for it first. ### Should I retry a request refused with 100? No. The content is at fault, or the target ID with subcode 33: the same request will produce the same error. Fix it first. ### What is the difference between error 100 and error 131009? Meta describes 100 as an unsupported or misspelled parameter, and 131009 as a known parameter whose value is invalid. Both are fixed by reading `details`. ### Genuka WA accepted my request, so why does Meta refuse it? Genuka checks the shape of the request; Meta also checks what only it knows, such as endpoint-specific parameters or the state of the account. The `traceId` relayed in `meta` is what Meta support asks for. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Error codes, Marketing Messages API for WhatsApp](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#marketing-messages-api-for-whatsapp-error-codes) * [Activepieces community — code 100, error\_subcode 33](https://community.activepieces.com/t/whatsapp-business-error/4324) * [Fast2SMS — Unsupported POST/GET request, object does not exist](https://www.fast2sms.com/help/?p=17132) --- # WhatsApp error 131026: Message undeliverable — fix URL: https://wa.genuka.com/en/docs/errors/131026 Language: English > WhatsApp error 131026 (Message Undeliverable): number not on WhatsApp, terms not accepted or app too old. How to diagnose it and what to do next. WhatsApp error 131026 means Meta could not deliver the message to the recipient: the number is not on WhatsApp, the person has not accepted the latest terms of service, or their app is too old. Do not retry on WhatsApp. Check the number you stored, then reach the person through another channel such as SMS or email. ## What does error 131026 mean? Meta describes it this way ([Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#other-errors)): > "Unable to deliver message. Reasons can include: The recipient phone number is not a WhatsApp > phone number. Recipient has not accepted the new Terms of Service and Privacy Policy. Recipient > using an old WhatsApp version." Three possible causes, all on the recipient's side. For the third one, Meta lists the minimum app versions: | App | Minimum version | | --------------------------------- | --------------- | | Android | 2.21.15.15 | | SMBA (WhatsApp Business, Android) | 2.21.15.15 | | iOS | 2.21.170.4 | | SMBI (WhatsApp Business, iOS) | 2.21.170.4 | | KaiOS | 2.2130.10 | | Web | 2.2132.6 | The title that comes with the code in the `message` field is "Message Undeliverable" ([360dialog's list](https://docs.360dialog.com/docs/support/api-error-message-list)). It names the symptom, not the cause: the code and `error_data.details` are what tell you why. ## When does error 131026 happen? * **The number has no WhatsApp account**: a landline, a switchboard number, a typo in a form. * **The number is badly formatted**: without `+` and a country code, a local number can be read as belonging to another country and land on someone without WhatsApp. Always write `to` in international format, for example `+237690000001`. * **The recipient's app is out of date**, or they have not accepted the new terms of service. ### Where do you see it in Genuka WA? The most common case: Meta accepts the send, then reports the failure on the status webhook. | Channel | What you get | | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | `message_status` webhook | `data.status: "failed"`, `data.errors[0].code: 131026` | | `POST /api/v1/messages` response, when Meta refuses on the call | `400`, `"error": "send_recipient_permanent"`, `meta.code: 131026` | | Campaign | The recipient turns `failed`, with Meta's message in `errorMessage`; it is never retried | | Dashboard, Inbox section | The failed bubble; "See why" shows Meta's code and details | ```json title="message_status webhook received by your endpoint (excerpt)" { "id": "dlv_2f8c1a...", "type": "message_status", "field": "messages", "connection_id": "cn77...", "data": { "id": "wamid.HBgLMjM3...", "status": "failed", "timestamp": "1791451867", "recipient_id": "237690000001", "errors": [ { "code": 131026, "title": "…", "message": "…", "error_data": { "details": "…" }, "href": "/documentation/business-messaging/whatsapp/support/error-codes" } ] } } ``` `data` is Meta's status object, passed through untouched: `error_data.details` varies with the case and may name the cause. ## How do I fix error 131026? 1. **Do not retry on WhatsApp.** The same send will fail the same way. Genuka WA classifies 131026 as `recipient_permanent` and never retries it, in a campaign or a single send. 2. **Check the number as you stored it**: country code, extra or missing digits, a number typed into a "landline" field. 3. **Reach the person through another channel**, SMS or email, as Meta recommends, and ask them to check three things: that they can message your WhatsApp Business number themselves, that they have accepted the latest terms (Meta points to **Settings > Help** and **Settings > Application information**, which show the prompt when needed), and that their app is up to date. 4. **Mark the contact as unreachable on WhatsApp** in your database, and only re-enable it when they write to you: an inbound message proves they can receive. **Node.js** ```ts // app/api/webhooks/whatsapp/route.ts import { verifySignature } from "@genuka/whatsapp/webhooks"; export async function POST(request: Request) { const raw = await request.text(); const signature = request.headers.get("x-genuka-signature"); if (!(await verifySignature(process.env.WEBHOOK_SECRET!, raw, signature))) { return new Response("invalid signature", { status: 401 }); } const event = JSON.parse(raw); if (event.type === "message_status" && event.data.status === "failed") { const codes: number[] = (event.data.errors ?? []).map((e: { code: number }) => e.code); if (codes.includes(131026)) { // Yours to write: the contact moves to SMS/email until their next inbound message. void markWhatsAppUnreachable(event.data.recipient_id); } } return new Response("ok"); } ``` **Python** ```python import hashlib import hmac import json import os import time from flask import Flask, request app = Flask(__name__) def verify(raw: bytes, header: str, secret: str, tolerance: int = 300) -> bool: parts = dict(part.split("=", 1) for part in header.split(",") if "=" in part) timestamp = int(parts.get("t", "0")) if abs(time.time() - timestamp) > tolerance: return False expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts.get("v1", "")) @app.post("/webhooks/whatsapp") def whatsapp_webhook(): raw = request.get_data() if not verify(raw, request.headers.get("X-Genuka-Signature", ""), os.environ["WEBHOOK_SECRET"]): return "invalid signature", 401 event = json.loads(raw) if event["type"] == "message_status" and event["data"]["status"] == "failed": codes = [error["code"] for error in event["data"].get("errors", [])] if 131026 in codes: mark_whatsapp_unreachable(event["data"]["recipient_id"]) # your function return "ok" ``` ## How do I prevent error 131026? * **Validate numbers at input**, in international format with a country code. * **Get the contact to reach you on WhatsApp first** when you can, with a "Message us on WhatsApp" link: a number that has already written to you is a number that receives. * **Listen to the `message_status` webhook** rather than the send response: a `200` means "accepted by Meta", not "delivered". * **Keep a fallback channel** for critical messages such as login codes. The [WhatsApp OTP in Node.js guide](https://wa.genuka.com/en/docs/guides/whatsapp-otp-nodejs) plans for it. ## Related error codes * [131050](https://wa.genuka.com/en/docs/errors/131050): the recipient does receive WhatsApp, but stopped your marketing messages. * [131049](https://wa.genuka.com/en/docs/errors/131049): a marketing message held back by Meta for this recipient, to try again later. * [131047](https://wa.genuka.com/en/docs/errors/131047): the 24-hour window is closed; a template will go through. ## FAQ ### Should I retry a send that failed with 131026? Not on WhatsApp. Until the recipient's situation changes (WhatsApp installed, app updated, terms accepted), the same send will fail. ### Why was the send response 200 when the message failed? Because Meta accepted the request before attempting delivery. Meta states that its errors come back in the response, by webhook, or both, and recommends watching both. A 131026 most often arrives on the status webhook. ### How do I find the 131026 recipients of a campaign? `GET /api/v1/campaigns/{id}/recipients?status=failed&limit=1000` lists failed recipients with Meta's message, 200 by default and 1,000 at most, with no pagination. The numeric code arrives on the `message_status` webhook: store it there if you want to filter by cause, and it is the only complete list for a large campaign. ### Is a message that failed with 131026 billed? Not by Meta, which only charges for a template message when it is delivered ([Meta, Pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing)). On the Genuka WA side, however, a send Meta accepted counts against your subscription quota even if delivery fails afterwards: drop unreachable numbers from your next sends. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Status webhook reference](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status) * [Meta — Pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing) * [360dialog — API error message list](https://docs.360dialog.com/docs/support/api-error-message-list) --- # WhatsApp error 131042: payment method issue — fix URL: https://wa.genuka.com/en/docs/errors/131042 Language: English > WhatsApp error 131042: Meta refuses the send because the WhatsApp Business account has no valid payment method. Where to add the card and how to check it. WhatsApp error 131042 means Meta refuses the send because of the WhatsApp Business account's payment method: no card, a credit line over its limit, or an account that is not set up correctly. Meta bills messages directly to the client, not through Genuka. Add or fix the payment method at Meta, then send again. ## What does error 131042 mean? > "There was an error related to your payment method." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#other-errors) Meta asks you to verify the billing setup and lists the common problems: * the payment account is not attached to a WhatsApp Business account; * the credit line is over the limit; * the credit line (payment account) is not set or not active; * the WhatsApp Business account is deleted; * the WhatsApp Business account is suspended; * the timezone is not set; * the currency is not set; * the MessagingFor (On Behalf Of) request is pending or declined. The title that comes with the code in the `message` field is "Business eligibility payment issue" ([360dialog's list](https://docs.360dialog.com/docs/support/api-error-message-list)). ### Why isn't my Genuka WA subscription enough? Because Genuka is a **Meta Tech Provider**, not a Business Solution Provider (BSP). Each client connects its own WhatsApp Business account, and **Meta bills messages directly to that account**, with no markup from Genuka. The Genuka WA subscription pays for the platform, not for the messages: it does not replace the card Meta expects on the client's business portfolio. What we see in practice: an account with no payment method can create templates and get them approved, but every send comes back as 131042. And we cannot check it in advance, because Meta only shares an account's funding details with BSPs. ## When does error 131042 happen? * **On the first template send** of a freshly connected account, when nobody has added a card at Meta yet. * **When a card expires or is declined** on an account that used to send. * **In the middle of a campaign**: every recipient fails for the same reason. ### Where do you see it in Genuka WA? | Channel | What you get | | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | | `POST /api/v1/messages` response | A refusal whose `meta.code` is `131042`, with Meta's text in `message` | | `message_status` webhook, when Meta reports the failure afterwards | `data.status: "failed"`, `data.errors[0].code: 131042` | | Campaign | Every recipient turns `failed`. When Meta refuses on the call, the incident is also flagged once for the campaign | | Dashboard, client page, when Meta refuses on the call | The "Meta is refusing sends on this number" notice, with Meta's message and an "Add a card on Meta" button | | WhatsApp, when Meta refuses on the call | An alert to the partner, at most once a week per number | Meta states that its errors come back in the response, by webhook, or both ([Meta, error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes)). Genuka WA only raises the notice and the WhatsApp alert for a refusal received **on the call**: a 131042 that only arrives on the `message_status` webhook turns the message `failed`, with no notice and no alert. So watch the webhook too. ## How do I fix error 131042? 1. **Open the account's payment settings at Meta.** From the client page in the dashboard, the "Add a card on Meta" button opens the WhatsApp account page in Meta's business settings directly, or the billing hub when the portfolio ID is unknown. The account owner, the client, has to act: the card is theirs. 2. **Add or replace the payment method**, then check the other causes on Meta's list: timezone and currency set, account neither suspended nor deleted. 3. **Confirm it in Genuka WA** with "A card is already registered". We cannot check it ourselves: the notice comes back on its own if Meta refuses another send on the call for the same reason. 4. **Test with a single message** before relaunching a campaign: a send that is accepted and then delivered confirms the block is gone. **Node.js** ```ts const response = await fetch("https://wa.genuka.com/api/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ connectionId: "con_1", to: "+237690000001", template: { name: "order_update", language: "en", body: ["Awa", "ORD-1042"] }, }), }); const json = await response.json(); if (!response.ok && json.meta?.code === 131042) { // Nothing to retry: the client has to add a card to their Meta account. await notifyAccountOwner(json.meta.details ?? json.message); // your function } // In your webhook route, once the signature is verified: the same refusal can arrive afterwards. export async function onMessageStatus(event: { type: string; data: { status: string; errors?: { code: number; message?: string }[] }; }) { if (event.type !== "message_status" || event.data.status !== "failed") return; const error = event.data.errors?.find((e) => e.code === 131042); if (error) await notifyAccountOwner(error.message ?? "131042"); } ``` **Python** ```python import os import requests response = requests.post( "https://wa.genuka.com/api/v1/messages", headers={ "Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}", "User-Agent": "acme-crm/1.0 (+https://example.com)", }, json={ "connectionId": "con_1", "to": "+237690000001", "template": {"name": "order_update", "language": "en", "body": ["Awa", "ORD-1042"]}, }, timeout=30, ) body = response.json() if not response.ok and body.get("meta", {}).get("code") == 131042: # Nothing to retry: the client has to add a card to their Meta account. notify_account_owner(body["meta"].get("details") or body.get("message")) # your function # In your webhook route, once the signature is verified: the same refusal can arrive afterwards. def on_message_status(event: dict) -> None: data = event.get("data", {}) if event.get("type") != "message_status" or data.get("status") != "failed": return for error in data.get("errors", []): if error.get("code") == 131042: notify_account_owner(error.get("message") or "131042") return ``` ## How do I prevent error 131042? * **Have the card added during onboarding**, before the first template. The dashboard shows the "Add a payment method on Meta" notice on each number until it is confirmed. * **Explain the two bills to your clients from the start**: the Genuka WA subscription on one side, messages billed by Meta on the other. * **Do not launch a large campaign on an account that has never sent**: a single test message reveals the problem for the price of one message. ## Related error codes * [131048](https://wa.genuka.com/en/docs/errors/131048): another reason Meta refuses a number's sends, tied to quality rather than payment. * [132001](https://wa.genuka.com/en/docs/errors/132001): a refusal about the template itself, sometimes mistaken for an account block. For the Genuka WA subscription itself, see [Plans and billing](https://wa.genuka.com/en/docs/billing). ## FAQ ### Does my Genuka WA subscription cover WhatsApp messages? No. Genuka charges a subscription per WhatsApp number; messages are billed by Meta, directly to the client's WhatsApp Business account, with no markup from Genuka. See the [plans](https://wa.genuka.com/en#pricing). ### Why can't Genuka WA detect the missing card before sending? Because Meta only shares a WhatsApp Business account's funding details with BSPs. Genuka being a Tech Provider, the first reliable signal is the 131042 refusal itself. ### Who has to add the card? The business that owns the WhatsApp Business account, in its own Meta business portfolio. A partner managing several clients cannot pay on their behalf through Genuka WA. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [360dialog — API error message list](https://docs.360dialog.com/docs/support/api-error-message-list) --- # WhatsApp error 131047: Re-engagement message — fix URL: https://wa.genuka.com/en/docs/errors/131047 Language: English > WhatsApp error 131047 (Re-engagement message) means the 24-hour window is closed. Why Meta refuses the free-form message and how to switch to a template. WhatsApp error 131047 means more than 24 hours have passed since the recipient last messaged you: the customer service window is closed, and only an approved template can still reach them. The fix is simple: resend the content as a template, then go back to free-form messages as soon as the customer replies. ## What does error 131047 mean? > "More than 24 hours have passed since the recipient last replied to the sender number." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#other-errors) Meta's suggested fix is one line: "Send the recipient a template message instead." In the response's `message` field, the code is followed by its title, "Re-engagement message" ([360dialog's list](https://docs.360dialog.com/docs/support/api-error-message-list)); Meta warns these titles will be deprecated, so do not code against them. The rule behind the code is the **customer service window**. When a user messages or calls you, a 24-hour timer starts; each new message or call from them resets it to 24 hours. While it runs you can send any free-form message: text, image, buttons, list. Once it expires, only approved templates go through ([Meta, Send messages](https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/send-messages#customer-service-windows)). ## When does error 131047 happen? * **A free-form message sent too late**: the support answer goes out the next morning, while the customer wrote at 8 a.m. the day before. * **A first free-form message** to someone who never wrote to you: there is no window at all. * **A Flow sent as an interactive message** outside the window: same rule, see [Flows in the library](https://wa.genuka.com/sdk/flows). * **Blocking a user**: Meta only lets you block a number that messaged you in the last 24 hours ([Meta, Block users](https://developers.facebook.com/documentation/business-messaging/whatsapp/block-users)). `POST /api/v1/blocked-users` then reports the failure number by number, with code 131047 and the message "Failed to block due to re-engagement check failed", which we recorded on a live number on August 13, 2026. ### Why does the API answer 200 when the message never arrives? This is the main trap with this code. On August 13, 2026, on a live number, we sent three free-form texts outside the window: Meta answered **HTTP 200 with a valid `wamid`** for each, and none was delivered. No 131047 on the call. A template sent to the same recipient right after arrived immediately. Depending on the case, the error comes back on the call, later on the status webhook, or not at all. That is why Genuka WA checks the window on its side before every free-form message and says so in the response, without blocking the send: ```json title="200 — POST /api/v1/messages outside the window" { "data": { "messageId": "wamid.HBgLMjM3...", "warning": { "code": "outside_service_window", "message": "This contact last messaged you at 2026-10-06T08:12:44.000Z, more than 24 hours ago, so the service window is closed. Meta will most likely drop this message; send an approved template instead." } } } ``` ### Where do you see it in Genuka WA? | Channel | What you get | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `200` response of `POST /api/v1/messages` | `data.warning.code`: `outside_service_window` (last inbound message more than 24 h ago) or `unverified_service_window` (no inbound message on record for this contact) | | `400` response of `POST /api/v1/messages` | `"error": "send_needs_template"`, `meta.code: 131047`, when Meta refuses on the call | | `message_status` webhook | `data.status: "failed"` and `data.errors[0].code: 131047` | | Dashboard, Inbox section | The failed bubble; "See why" shows Meta's code and details | The `warning` is never a refusal: our record of the window comes from the webhook feed, and an outage on our side must not block a send Meta would have delivered. The decision is yours. ## How do I fix error 131047? 1. **Do not resend the free-form message.** It will fail exactly the same way. Genuka WA classifies 131047 as `needs_template` and never retries it, in a single send or in a campaign. 2. **Pick an approved template** of the right kind: `UTILITY` for order or account information, `MARKETING` for a promotion. The list is at `GET /api/v1/templates?status=approved` for templates created through Genuka WA; those created in WhatsApp Manager are not in it, but send the same way. 3. **Send it with the `template` field** on the same endpoint, always passing `language`. 4. **Go back to free-form when the customer replies.** Their reply opens a new 24-hour window. **curl** ```bash curl -X POST https://wa.genuka.com/api/v1/messages \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "to": "+237690000001", "template": { "name": "order_update", "language": "en", "body": ["Awa", "ORD-1042"] } }' # { "data": { "messageId": "wamid.HBg..." } } ``` **Node.js** ```ts const API = "https://wa.genuka.com/api/v1"; const WINDOW_MS = 24 * 60 * 60 * 1000; async function post(path: string, body: unknown) { const response = await fetch(`${API}${path}`, { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); const json = await response.json(); if (!response.ok) throw Object.assign(new Error(json.message ?? json.error), json); return json.data as { messageId: string; warning?: { code: string; message: string } }; } // lastInboundAt: when this contact last wrote to you, kept up to date by your webhooks. export async function notify(to: string, text: string, lastInboundAt: Date | null) { const windowOpen = lastInboundAt !== null && Date.now() - lastInboundAt.getTime() < WINDOW_MS; const template = { connectionId: "con_1", to, template: { name: "order_update", language: "en", body: ["Awa", "ORD-1042"] }, }; if (!windowOpen) return post("/messages", template); try { return await post("/messages", { connectionId: "con_1", to, text }); } catch (error) { if ((error as { error?: string }).error === "send_needs_template") return post("/messages", template); throw error; } } ``` **Python** ```python import os import requests response = requests.post( "https://wa.genuka.com/api/v1/messages", headers={ "Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}", "User-Agent": "acme-crm/1.0 (+https://example.com)", }, json={ "connectionId": "con_1", "to": "+237690000001", "template": {"name": "order_update", "language": "en", "body": ["Awa", "ORD-1042"]}, }, timeout=30, ) response.raise_for_status() print(response.json()["data"]["messageId"]) ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_USERAGENT => "acme-crm/1.0 (+https://example.com)", CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("GENUKA_WA_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "connectionId" => "con_1", "to" => "+237690000001", "template" => ["name" => "order_update", "language" => "en", "body" => ["Awa", "ORD-1042"]], ]), ]); $json = json_decode(curl_exec($ch), true); curl_close($ch); echo $json["data"]["messageId"] ?? $json["error"]; ``` If the free-form message already went out and a `message_status` webhook comes back `failed` with code 131047, the same rule applies: resend the content as a template, once. ## How do I prevent error 131047? * **Keep your own window clock.** Every inbound message reaches your webhook with the sender and a timestamp: store the latest one per contact and choose template or free-form **before** sending. * **Read `data.warning`.** When it is present, assume the free-form message will probably not be delivered, and watch its status. * **Use a template for anything you initiate**: order confirmation, appointment reminder, login code. The [order notifications guide](https://wa.genuka.com/en/docs/guides/order-notifications) is built on that principle. * **Keep a re-engagement template** in the `UTILITY` category, short, inviting the customer to reply so the conversation reopens. ## Related error codes * [132001](https://wa.genuka.com/en/docs/errors/132001): the template you picked to re-engage does not exist in that language or is not approved. * [132000](https://wa.genuka.com/en/docs/errors/132000): the template is found, but the number of variables does not match. * [131050](https://wa.genuka.com/en/docs/errors/131050): the recipient stopped marketing messages; a `MARKETING` template will not go through either. * [131026](https://wa.genuka.com/en/docs/errors/131026): the recipient cannot receive any message, templates included. ## FAQ ### Do the 24 hours start from my last message or theirs? Theirs. The window starts at the **customer's** last message or call; the messages you send do not extend it. ### Does a template reopen the 24-hour window? No, the customer's reply does. Meta spells it out for marketing templates: if the user responds, a 24-hour customer service window starts ([Meta, Per-user marketing limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/per-user-limits)). ### Is a free-form message sent inside the window billed? Meta marks it `free_customer_service` in the status webhook: a non-template message sent within a customer service window is free, as is a `UTILITY` template sent within the window ([Meta, status webhook](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status)). Templates are billed by Meta directly to the client's WhatsApp Business account; Genuka WA adds no markup. ### Can I block a number that never wrote to me? No. Blocking requires an inbound message from the last 24 hours, otherwise Meta answers 131047 for that number. `POST /api/v1/blocked-users` returns `207` when some numbers went through, `422` when none did. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Send messages, customer service windows](https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/send-messages#customer-service-windows) * [Meta — Status webhook reference](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status) * [Meta — Per-user marketing template message limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/per-user-limits) * [Meta — Block users](https://developers.facebook.com/documentation/business-messaging/whatsapp/block-users) * [360dialog — API error message list](https://docs.360dialog.com/docs/support/api-error-message-list) --- # WhatsApp error 131049: marketing message not delivered URL: https://wa.genuka.com/en/docs/errors/131049 Language: English > WhatsApp error 131049: Meta held back your marketing template to keep engagement healthy. The per-user limit, and when to resend without making it worse. WhatsApp error 131049 means Meta chose not to deliver a marketing template to this recipient, most often because they reached their marketing message limit for now. It is neither a bug nor a penalty on your number. Wait at least 24 hours before trying that recipient again, unless they have a US number: there, waiting does not help. ## What does error 131049 mean? > "This message was not delivered to maintain healthy ecosystem engagement." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#other-errors) Meta adds: "If you do receive this error code and suspect it is due to the limit, wait at least 24 hours before resending the template message." The limit in question is the **per-user marketing template message limit** ([Meta, Per-user marketing template message limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/per-user-limits)): * WhatsApp may limit how many marketing templates a person receives **from any business** over a period, when they are less likely to engage with them. * The limit adapts to each person: their recent marketing read rate, and how many messages they already have in their inbox from friends, family and businesses. * Every **delivered** marketing template counts. If the person replies, a 24-hour customer service window opens, and marketing messages sent within that window do not count. * Resending several times within 24 hours to someone who reached their limit can make that recipient unreachable for up to another 24 hours, with the same 131049. Your other recipients are not affected. * The limit is not active for messages sent from a number in the European Economic Area, the United Kingdom, Japan or South Korea, nor to a user in those countries. ### Why do my US recipients always fail? Because Meta no longer delivers any marketing template to them. On the same page: > "WhatsApp does not currently deliver marketing template messages to WhatsApp users with United > States phone numbers (numbers composed of a +1 dialing code and a US area code)." This is **not** the per-user limit: the block does not lift after 24 hours, and Meta announces no end date. Meta only says "an error", without a code; integrators report it as 131049, in place since April 1, 2025 ([Message Central](https://www.messagecentral.com/blog/whatsapp-marketing-usa-allowed)). Only US area codes are concerned, not every +1 number. * **Remove those numbers from your `MARKETING` campaigns.** In a campaign, a 131049 received on the call leaves the recipient `pending`: a US number will stay there on every relaunch. * **Use another kind of message**: a `UTILITY` or `AUTHENTICATION` template when the content fits (those categories are not affected, according to the same source), or a reply inside the 24-hour service window a customer message opens. * **Do not reschedule them every 24 hours**: every send Meta accepts before failing it counts against your Genuka WA subscription quota. ## When does error 131049 happen? It only concerns `MARKETING` templates, and it arrives on the status webhook: Meta says so explicitly, the failure is reported by the `messages` webhook with status `failed`. Here is Meta's own example, as Genuka WA forwards it to you in `data`: ```json title="message_status webhook (excerpt)" { "type": "message_status", "field": "messages", "data": { "id": "wamid.HBgLMTY1MDM4Nzk0MzkVAgARGBI0QUQ2MjA4NEYyRkExNjMyREUA", "status": "failed", "timestamp": "1751142888", "recipient_id": "16505551234", "errors": [ { "code": 131049, "title": "This message was not delivered to maintain healthy ecosystem engagement.", "message": "This message was not delivered to maintain healthy ecosystem engagement.", "error_data": { "details": "In order to maintain a healthy ecosystem engagement, the message failed to be delivered." }, "href": "/documentation/business-messaging/whatsapp/support/error-codes" } ] } } ``` ### Where do you see it in Genuka WA? | Channel | What you get | | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | `message_status` webhook | `data.status: "failed"`, `data.errors[0].code: 131049` | | Campaign, failure reported by webhook | The recipient turns `failed`, with Meta's message in `errorMessage` | | Campaign, refused on the call | The recipient stays `pending`: it is not counted as failed, and a new launch picks it up | | `POST /api/v1/messages` response, when Meta refuses on the call | `400`, `"error": "send_recipient_throttled"`, `meta.code: 131049` | Genuka WA classifies this code as `recipient_throttled`: the message is fine, the recipient is fine, the timing is wrong. It is never retried right away, since that would only extend the block. ## How do I fix error 131049? 1. **Send nothing for 24 hours.** It is Meta's only instruction, and the one most often ignored: an immediate resend does not go through and can extend the block. 2. **Set those recipients aside.** From the webhook, store the number and the time of the failure. `GET /api/v1/campaigns/{id}/recipients?status=failed&limit=1000` also gives you the list on the campaign side, capped at 1,000 rows with no pagination: beyond that, rely on what you stored from the webhook. 3. **Try them again later, in a new campaign**, at least 24 hours after the failure, without the US numbers. A campaign that was already launched only resends its recipients that are still `pending`. **Node.js** ```ts import { parsePhoneNumberFromString } from "libphonenumber-js"; const RETRY_AFTER_MS = 24 * 60 * 60 * 1000; // Called by your webhook route, once the signature is verified. export async function onMessageStatus(event: { type: string; data: { status: string; recipient_id: string; errors?: { code: number }[] }; }) { if (event.type !== "message_status" || event.data.status !== "failed") return; if (!event.data.errors?.some((error) => error.code === 131049)) return; // A US number will not receive marketing in 24 hours either: take it out of your campaigns. if (parsePhoneNumberFromString(`+${event.data.recipient_id}`)?.country === "US") { await db.marketingExclusion.upsert({ waId: event.data.recipient_id, reason: "us_marketing" }); return; } // Your storage: a "retry later" table with the earliest time a resend is allowed. await db.marketingRetry.upsert({ waId: event.data.recipient_id, notBefore: new Date(Date.now() + RETRY_AFTER_MS), }); } ``` **Python** ```python from datetime import datetime, timedelta, timezone import phonenumbers RETRY_AFTER = timedelta(hours=24) # Called by your webhook route, once the signature is verified. def on_message_status(event: dict) -> None: data = event.get("data", {}) if event.get("type") != "message_status" or data.get("status") != "failed": return if not any(error.get("code") == 131049 for error in data.get("errors", [])): return # A US number will not receive marketing in 24 hours either: take it out of your campaigns. number = phonenumbers.parse("+" + data["recipient_id"]) if phonenumbers.region_code_for_number(number) == "US": exclude_from_marketing(data["recipient_id"]) # your function return # Your storage: a "retry later" table with the earliest time a resend is allowed. save_marketing_retry( wa_id=data["recipient_id"], not_before=datetime.now(timezone.utc) + RETRY_AFTER, ) ``` ## How do I prevent error 131049? * **Target people who read.** The limit follows each recipient's recent read rate: a list of active customers does better than your whole file. * **Invite replies.** A reply opens a 24-hour window in which your marketing messages no longer count against the limit. * **Space out campaigns** to the same people, and drop from your lists those who keep collecting 131049\. * **Measure delivery on webhooks**, not on accepted sends: a marketing campaign can show 100% accepted and a share of 131049 on arrival. ## Related error codes * [131050](https://wa.genuka.com/en/docs/errors/131050): the person stopped your marketing messages; never resend. * [131048](https://wa.genuka.com/en/docs/errors/131048): **your number** is restricted because of spam reports, not a single recipient. * [131026](https://wa.genuka.com/en/docs/errors/131026): the recipient cannot receive anything at all. ## FAQ ### Does error 131049 penalize my number? No. Meta states that the block caused by excessive resends does not affect your ability to send marketing messages to other users. It is a per-recipient limit. ### Are utility and authentication templates affected? The limit Meta describes applies to marketing templates. A login code or an order confirmation in an `AUTHENTICATION` or `UTILITY` template is not subject to it. ### How long does the limit last? Meta gives no fixed duration: the limit adapts to each person's engagement. The rule is not to resend before at least 24 hours. ### Why do some of my European recipients never get 131049? Because the limit is not active for users in the European Economic Area, the United Kingdom, Japan and South Korea, nor for messages sent from a number in those countries. ### Is a marketing template that failed with 131049 billed? Not by Meta, which only charges for a template message when it is delivered ([Meta, Pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing)). On the Genuka WA side, the send Meta accepted has already counted against your subscription quota: resending too early costs a message of quota and delivers nothing. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Per-user marketing template message limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/per-user-limits) * [Meta — Status webhook reference](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status) * [Meta — Pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing) * [Message Central — WhatsApp marketing in the USA](https://www.messagecentral.com/blog/whatsapp-marketing-usa-allowed) --- # WhatsApp error 131050: user stopped marketing messages URL: https://wa.genuka.com/en/docs/errors/131050 Language: English > WhatsApp error 131050: the recipient stopped your marketing messages. Why you must never retry, and how Genuka WA blocks the sends that would follow. WhatsApp error 131050 means the recipient chose to stop receiving your marketing messages on WhatsApp. Do not retry: the message will not be received. Remove them from your marketing sends, and only add them back if they turn marketing messages on again themselves, which Meta tells you about through a webhook. ## What does error 131050 mean? > "Unable to deliver the message. This recipient has chosen to stop receiving marketing messages > on WhatsApp from your business." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#other-errors) Meta's instruction leaves no room: "Do not retry sending messages to this user as they will not be received." To learn about it ahead of time rather than after a failed send, Meta points to the **`user_preferences`** webhook, which fires when a user stops or resumes marketing messages from your business. It does not fire for "Interested" or "Not interested" feedback given through the *Offers and announcements* setting ([Meta, user\_preferences webhook](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/user_preferences)). ## When does error 131050 happen? When you send a `MARKETING` template to someone who stopped the business's marketing messages. With Genuka WA you mostly see it in two cases: the opt-out predates the number's connection, so Genuka WA never received the `user_preferences` webhook announcing it; or the template was created outside Genuka WA, and its category is unknown to us. ### What does Genuka WA do on its side? An opt-out is a compliance boundary, not an optimization. Genuka WA treats it that way: * **Two sources feed the same record**, per client: Meta's `user_preferences` webhook (`stop` or `resume`), and any 131050 reported by a send's status webhook. An opt-in can only come from a `resume`: a 131050 is never read as consent. * **Single send**: a `MARKETING` template known to Genuka WA, addressed to an opted-out contact, is refused **before** it reaches Meta, with `403 recipient_opted_out`. Nothing is sent, nothing is counted against your quota. * **`MARKETING` campaign**: opted-out contacts are excluded before the first send, marked `skipped` with the reason "Recipient opted out of marketing messages", and counted in the `skipped` field of the launch response. * **What is not blocked**: `UTILITY` and `AUTHENTICATION` templates, and free-form replies inside the 24-hour window. Opting out of marketing is not opting out of being answered. ### Where do you see it in Genuka WA? | Channel | What you get | | --------------------------------- | -------------------------------------------------------------------------- | | `POST /api/v1/messages` response | `403`, `"error": "recipient_opted_out"`, when the opt-out is already known | | `message_status` webhook | `data.status: "failed"`, `data.errors[0].code: 131050`, when Meta refuses | | `user_preference.stopped` webhook | `data.waId`, `data.value: "stop"`, `data.timestamp` | | Campaign | `skipped` recipients, with the reason in `errorMessage` | ```json title="403 — POST /api/v1/messages" { "error": "recipient_opted_out", "message": "This recipient opted out of marketing messages" } ``` ## How do I fix error 131050? 1. **Stop every marketing send to this contact.** Genuka WA classifies 131050 as `recipient_permanent`: it is never retried. 2. **Record the opt-out in your own tool** (CRM, customer database), so it survives an export or a campaign built somewhere else. 3. **Receive the `user_preference.stopped` and `user_preference.resumed` events.** An endpoint with no event filter already gets them; a filtered endpoint must add them to its `events` list through `PATCH /api/v1/webhooks/{id}`. Only opt the contact back in on a `resumed`. 4. **Treat `403 recipient_opted_out` as a normal answer**, not an outage: the contact can still be reached with a utility template and in reply to their messages. **Node.js** ```ts // 1. On send: an opt-out refusal is not an error to page anyone about. const response = await fetch("https://wa.genuka.com/api/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ connectionId: "con_1", to: "+237690000001", template: { name: "october_promo", language: "en", body: ["Awa"] }, }), }); if (response.status === 403) { const { error } = await response.json(); if (error === "recipient_opted_out") await crm.setMarketingConsent("+237690000001", false); } // 2. In your webhook route, once the signature is verified. export async function onEvent(event: { type: string; data: { waId?: string } }) { if (event.type === "user_preference.stopped") await crm.setMarketingConsent(event.data.waId!, false); if (event.type === "user_preference.resumed") await crm.setMarketingConsent(event.data.waId!, true); } ``` **Python** ```python # In your webhook route, once the signature is verified. def on_event(event: dict) -> None: kind = event.get("type") data = event.get("data", {}) if kind == "user_preference.stopped": crm.set_marketing_consent(data["waId"], False) elif kind == "user_preference.resumed": crm.set_marketing_consent(data["waId"], True) elif kind == "message_status" and data.get("status") == "failed": if any(error.get("code") == 131050 for error in data.get("errors", [])): crm.set_marketing_consent(data["recipient_id"], False) ``` ## How do I prevent error 131050? * **Collect explicit consent** before any marketing, naming your business and what you will send. * **Listen to `user_preferences` from day one**: an opt-out learned by webhook prevents the failed send, one learned through 131050 arrives after the fact. * **Create your templates through Genuka WA** (`POST /api/v1/templates`): their category is then known, and the preventive `403 recipient_opted_out` applies. * **Send less, but better.** An opt-out is a customer's answer to messages they did not want; frequency and relevance are your levers. ## Related error codes * [131049](https://wa.genuka.com/en/docs/errors/131049): a marketing message held back for this recipient, but only temporarily; resending after 24 hours is allowed. * [131026](https://wa.genuka.com/en/docs/errors/131026): the recipient receives no message at all, whatever its category. * [131047](https://wa.genuka.com/en/docs/errors/131047): the 24-hour window is closed for a free-form message. ## FAQ ### Can I still send an order confirmation to an opted-out contact? Genuka WA does not block `UTILITY` or `AUTHENTICATION` templates for a contact who opted out of marketing: Meta's 131050 is about marketing messages. The order confirmation then has to be a genuine utility template, not a disguised promotion. ### Does the opt-out apply to one number or to the whole business? Meta speaks of marketing messages "from your business". Genuka WA records it per client: every number of that client is covered. ### What happens with a template created outside Genuka WA? Genuka WA does not know its category and lets it through. If Meta reports a 131050, the opt-out is still recorded: your marketing campaigns and the marketing templates created through Genuka WA will exclude that contact from then on. The unknown template itself keeps going through to Meta. ### How does a contact opt back in? By turning your business's marketing messages back on in WhatsApp. Meta then sends the `user_preferences` webhook with the value `resume`, which Genuka WA forwards to you as `user_preference.resumed` and applies to its record. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — user\_preferences webhook reference](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/user_preferences) * [Meta — About the platform, user opt-in](https://developers.facebook.com/documentation/business-messaging/whatsapp/about-the-platform) --- # WhatsApp error 132000: template param count mismatch URL: https://wa.genuka.com/en/docs/errors/132000 Language: English > WhatsApp error 132000 (Template Param Count Mismatch): the variables you sent do not match the template. How to count them and send them correctly. WhatsApp error 132000 means the number of values you sent for the template's variables does not match the number of variables the template contains. Meta refuses the message without sending it. Count the variables of the approved template, header and body, then send exactly one value per variable, in the right order. ## What does error 132000 mean? > "The number of variable parameter values included in the request did not match the number of > variable parameters defined in the template." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#other-errors) Meta asks you to make sure the request includes a value for **every** parameter the template requires. The title that comes with the code in the `message` field is "Template Param Count Mismatch" ([360dialog's list](https://docs.360dialog.com/docs/support/api-error-message-list)). A template declares its variables in a format chosen at creation ([Meta, Templates, parameter formats](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/overview)): | Format | How it is written in the template | Rule at send time | | -------------------- | -------------------------------------------- | ---------------------------------------------------- | | Positional (default) | `{{1}}`, `{{2}}`… | Values follow the order of the variables in the text | | Named | `{{first_name}}` (lowercase and underscores) | Values can come in any order | ## When does error 132000 happen? * **A forgotten header variable**: the title "Order `{{1}}` shipped" has its own variable, which is not filled by the body's. * **A template edited after the integration**: a variable was added to the text, the code still sends the old number of values. * **A campaign with uneven variables**: some recipients have two values, others one. * **An empty or missing array** because a piece of data was missing in your app (unknown first name, an order number filtered out as `null`). ### Where do you see it in Genuka WA? | Channel | What you get | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `POST /api/v1/messages` response | `400`, `"error": "send_template"`, `meta.code: 132000`, `meta.retryable: false` | | Campaign | The affected recipients turn `failed`; if the variables have the same shape for everyone, **the whole** campaign fails the same way | Genuka WA classifies every 132xxx code as `template`: the error is in the request, and sending it again unchanged will always fail. ## How do I fix error 132000? 1. **Read the template as approved** with `GET /api/v1/templates/{id}`: the `components` field holds the header and body text. 2. **Count each component's variables**: the text header's on one side, the body's on the other. 3. **Put each value in the right field** of `template`: | In the template | In `POST /api/v1/messages` | | -------------------------------- | --------------------------------------------------------- | | Positional body `{{1}}`, `{{2}}` | `body` (or its alias `variables`): an array, in order | | Named body `{{first_name}}` | `bodyNamed`: an object `{ "first_name": "Awa" }` | | Text header with `{{1}}` | `header: { "text": "ORD-1042" }` | | Image, video or document header | `header: { "image": { "assetId": "ast_1" } }` | | Dynamic URL button | `buttons: [{ "type": "url", "text": "1042" }]` | | Authentication template | `otp: "472913"`, which fills both the body and the button | **curl** ```bash # 1. The template as approved curl https://wa.genuka.com/api/v1/templates/tpl_123 \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" # "components": [ # { "type": "HEADER", "format": "TEXT", "text": "Order {{1}} shipped" }, # { "type": "BODY", "text": "Hi {{1}}, your parcel arrives {{2}}." } # ] # 2. One value for the header, two for the body curl -X POST https://wa.genuka.com/api/v1/messages \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "to": "+237690000001", "template": { "name": "parcel_shipped", "language": "en", "header": { "text": "ORD-1042" }, "body": ["Awa", "tomorrow"] } }' ``` **Node.js** ```ts type Component = { type: string; format?: string; text?: string }; const VARIABLE = /\{\{\s*([a-z0-9_]+)\s*\}\}/g; /** How many values the template expects, per component. */ export function expectedParams(components: Component[]) { const count = (type: string) => { const component = components.find((c) => c.type.toUpperCase() === type); if (!component?.text) return 0; if (type === "HEADER" && component.format && component.format.toUpperCase() !== "TEXT") return 0; return new Set([...component.text.matchAll(VARIABLE)].map((match) => match[1])).size; }; return { header: count("HEADER"), body: count("BODY") }; } // Before sending: refuse locally rather than letting Meta answer 132000. const { data: template } = await fetch("https://wa.genuka.com/api/v1/templates/tpl_123", { headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}` }, }).then((response) => response.json()); const expected = expectedParams(template.components); const body = ["Awa", "tomorrow"]; if (body.length !== expected.body) { throw new Error(`The body expects ${expected.body} value(s), got ${body.length}`); } ``` **Python** ```python import re VARIABLE = re.compile(r"\{\{\s*([a-z0-9_]+)\s*\}\}") def expected_params(components: list[dict]) -> dict: """How many values the template expects, per component.""" def count(kind: str) -> int: component = next((c for c in components if c.get("type", "").upper() == kind), None) if not component or not component.get("text"): return 0 if kind == "HEADER" and component.get("format", "TEXT").upper() != "TEXT": return 0 return len(set(VARIABLE.findall(component["text"]))) return {"header": count("HEADER"), "body": count("BODY")} ``` ## How do I prevent error 132000? * **Validate before sending**, with a counter like the one above, and refuse an incomplete request locally: the error shows up earlier and names the missing data. * **Version your templates** (`parcel_shipped_v2`) when you change their variables, rather than editing a template your code already relies on. * **Check every campaign recipient** before `POST /api/v1/campaigns`: the same number of values for everyone, no empty value. * **Check the format too**: a template created with named parameters is filled with `bodyNamed`, not with an array. ## Related error codes * [132001](https://wa.genuka.com/en/docs/errors/132001): the template itself cannot be found in that language, or is not approved. * [132015](https://wa.genuka.com/en/docs/errors/132015): the template is paused for low quality. * [131047](https://wa.genuka.com/en/docs/errors/131047): the 24-hour window is closed, hence the switch to a template. ## FAQ ### Do the examples given at creation count at send time? No. Examples are there for Meta's review of the template; at send time you provide the real values, one per variable. ### Does the order of values matter? For a positional template, yes: the first value fills `{{1}}`, the second `{{2}}`. For a named template, no: Meta accepts values in any order. ### Why does my whole campaign fail with 132000? Because the error comes from the shape of the variables, which is the same for every recipient. Genuka WA does not retry a template error, and a campaign that was already launched only resends its recipients that are still `pending`: fix the variables, then create a new campaign. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Templates overview, parameter formats](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/overview) * [360dialog — API error message list](https://docs.360dialog.com/docs/support/api-error-message-list) --- # WhatsApp error 132001: template does not exist — fix URL: https://wa.genuka.com/en/docs/errors/132001 Language: English > WhatsApp error 132001 (Template does not exist): no approved template in that language. The en vs en_US trap, and how to check name, language and status. WhatsApp error 132001 means Meta cannot find an approved template with this name in this language, on the WhatsApp Business account doing the sending. Most of the time it is the language code: `en` and `en_US` are two different templates. Check the name, the exact language and the status, then send again. ## What does error 132001 mean? > "The template does not exist in the specified language or the template has not been approved." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#other-errors) Meta asks you to make sure the template has been approved, and that its name and language locale are correct. The title that comes with the code in the `message` field is "Template does not exist" ([360dialog's list](https://docs.360dialog.com/docs/support/api-error-message-list)). Three Meta rules explain most cases: * **A template is identified by its name and its language.** Creating the same name in several languages produces several templates, each counted separately ([Meta, Templates](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/overview)). * **Language codes are exact.** `en`, `en_US` and `en_GB` are three distinct languages, as are `fr`, `fr_BE`, `fr_CA` and `fr_CH` ([Meta, Supported languages](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/supported-languages)). * **A template belongs to a WhatsApp Business account.** It does not exist for a number attached to another account. A template inactive for 12 months or more is archived, then deleted 28 days later unless it is unarchived. ## When does error 132001 happen? * **The language you send is not the template's**, for example `en_US` for a template created as `en`, or `fr_CA` for a template created as `fr`. * **The language is left out on one side.** This is the trap specific to the Genuka WA API: without `language`, a template is **created** as `en`, but a message is **sent** as `en_US`. A template created and sent without a language therefore ends in 132001. * **The template is not approved yet**, or was rejected. * **The name differs**: a typo, a capital letter, a forgotten `_v2` suffix. * **The number is on the wrong account**: a template created for one client, sent from another client's number. ### Where do you see it in Genuka WA? Genuka WA keeps a mirror of the templates created through it, by API or from the dashboard. When it knows the requested template in that language, it refuses the send itself if the template is not approved, and names the reason. When it does not know it, for example a template created in WhatsApp Manager, the request goes to Meta, which sends it or answers 132001. | Situation | `POST /api/v1/messages` response | | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Template unknown to Genuka WA in that language | `400`, `"error": "send_template"`, `meta.code: 132001` (Meta's answer) | | Known template, under review | `400`, `"error": "send_template"`, `message`: `Template "x" is still under review by Meta` | | Known template, rejected | `400`, `"error": "send_template"`, `message`: `Template "x" was rejected during review and cannot be sent`, followed by the reason | | Known template, paused or disabled | `400`, `"error": "send_template"`, `meta.code`: [132015](https://wa.genuka.com/en/docs/errors/132015) or 132016. That 132016 can also mean a template Meta only flagged (`FLAGGED`): read it again, see [132015](https://wa.genuka.com/en/docs/errors/132015) | When creating a campaign, a template and a number that do not belong to the same client are refused upfront with `400 template_connection_mismatch`. ## How do I fix error 132001? 1. **Re-read your templates** with `GET /api/v1/templates?sync=true`, filtered by `companyId` if needed: each row gives an up-to-date `name`, `language` and `status`. That list only holds the templates created through Genuka WA: the sync refreshes their status but does not import the ones created in WhatsApp Manager. For those, check name, language and status in WhatsApp Manager. 2. **Compare character by character** the name and language of the row you want with what your code sends. 3. **Act on the status**: `pending`, wait for the review; `rejected`, fix it and submit again; missing in that language, including in WhatsApp Manager, create the translation with `POST /api/v1/templates`, same name, new `language`. 4. **Send again, always passing `language`**, with exactly the value of the row you found. **curl** ```bash curl "https://wa.genuka.com/api/v1/templates?sync=true&companyId=cmp_1&status=approved" \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" # { "data": [ { "id": "tpl_1", "name": "order_update", "language": "en", "status": "approved", ... } ] } ``` **Node.js** ```ts /** * The exact language a template created through Genuka WA is approved in, or an explicit error. * A template created in WhatsApp Manager is not in this list. */ export async function approvedLanguage(companyId: string, name: string, wanted: string) { const url = new URL("https://wa.genuka.com/api/v1/templates"); url.search = new URLSearchParams({ sync: "true", companyId, status: "approved" }).toString(); const response = await fetch(url, { headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}` }, }); const { data } = (await response.json()) as { data: { name: string; language: string }[] }; const languages = data.filter((t) => t.name === name).map((t) => t.language); if (languages.includes(wanted)) return wanted; throw new Error( languages.length === 0 ? `"${name}" not found among the approved templates in Genuka WA (check WhatsApp Manager)` : `"${name}" is approved in ${languages.join(", ")}, not in ${wanted}`, ); } ``` **Python** ```python import os import requests def approved_language(company_id: str, name: str, wanted: str) -> str: """The exact language a template created through Genuka WA is approved in. A template created in WhatsApp Manager is not in this list. """ response = requests.get( "https://wa.genuka.com/api/v1/templates", params={"sync": "true", "companyId": company_id, "status": "approved"}, headers={ "Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}", "User-Agent": "acme-crm/1.0 (+https://example.com)", }, timeout=60, ) response.raise_for_status() languages = [t["language"] for t in response.json()["data"] if t["name"] == name] if wanted in languages: return wanted if not languages: raise LookupError(f'"{name}" not found among the approved templates in Genuka WA (check WhatsApp Manager)') raise LookupError(f'"{name}" is approved in {", ".join(languages)}, not in {wanted}') ``` **PHP** ```php "true", "companyId" => "cmp_1", "status" => "approved"]); $ch = curl_init("https://wa.genuka.com/api/v1/templates?{$query}"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_USERAGENT => "acme-crm/1.0 (+https://example.com)", CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("GENUKA_WA_API_KEY")], ]); $templates = json_decode(curl_exec($ch), true)["data"]; curl_close($ch); foreach ($templates as $template) { echo "{$template['name']} [{$template['language']}] {$template['status']}\n"; } ``` ## How do I prevent error 132001? * **Store the name + language pair**, never the name alone, in your configuration. * **Always send `language`**, even when you think the default will do: it is not the same at creation (`en`) and at send time (`en_US`). * **Create every language you need**: a send in `fr_BE` fails if only the `fr` template exists. The simplest approach is often a single code per language, `fr` or `en`. * **Listen to the `message_template_status_update` webhook** and only use a new template once it is approved; `GET /api/v1/templates?sync=true` repairs a status that fell behind. ## Related error codes * [132000](https://wa.genuka.com/en/docs/errors/132000): the template is found, but the number of variables does not match. * [132015](https://wa.genuka.com/en/docs/errors/132015): the template exists and was approved, but it is paused. * [131047](https://wa.genuka.com/en/docs/errors/131047): the 24-hour window is closed, the usual reason for switching to a template. ## FAQ ### My template is approved in WhatsApp Manager, so why 132001? Three suspects, in this order: the language code you send is not exactly the template's, the sending number belongs to another WhatsApp Business account, or the name differs by one character. ### Are `en` and `en_US` the same language for Meta? No. They are two distinct codes in Meta's list, so two distinct templates. A template created as `en` does not answer a send in `en_US`. ### Can a template disappear without me deleting it? Yes. Meta archives a template that has been inactive for 12 months or more, then deletes it 28 days later unless it is unarchived. A rarely used re-engagement template is the typical candidate. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Templates overview](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/overview) * [Meta — Supported languages](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/supported-languages) * [360dialog — API error message list](https://docs.360dialog.com/docs/support/api-error-message-list) --- # WhatsApp error 132015: template paused — cause and fix URL: https://wa.genuka.com/en/docs/errors/132015 Language: English > WhatsApp error 132015: Meta paused the template for low quality. How long pauses last (3 h, 6 h, then disabled with 132016) and how to resume safely. WhatsApp error 132015 means the template is paused: its quality rating dropped to the lowest level after negative feedback or low read rates, and Meta suspended it. It cannot be sent while the pause lasts. Wait for the pause to lift or edit the template, and switch to another template in the meantime. ## What does error 132015 mean? > "Template is paused due to low quality so it cannot be sent in a template message." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#other-errors) Meta's fix: edit the template to improve its quality, then try again once it is approved. Its neighbouring code marks the next step: > "Template has been paused too many times due to low quality and is now permanently disabled." > — Meta, code 132016 For 132016 there is only one way out: create a new template, with different content. ### How does pausing work? Every template has a quality rating, based on usage, user feedback and engagement: `GREEN`, `YELLOW`, `RED`, or `UNKNOWN` until there is enough data ([Meta, Template quality](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/template-quality)). According to Meta's page on pausing, a template that reaches the lowest rating (`RED` through the API) is paused automatically ([Meta, Template pausing](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/template-pausing)). Its page on quality says, however, that a `RED` template "can be sent, but is in danger of being paused or disabled soon". Meta is not consistent from one page to the other: treat `RED` as an imminent pause. The pause schedule: | Occurrence | Consequence | | ---------- | ---------------------- | | 1st time | Paused for 3 hours | | 2nd time | Paused for 6 hours | | 3rd time | Disabled: error 132016 | During the pause, Meta does not charge you for send attempts and does not count them against your messaging limit, but the API rejects them. When the pause ends, the template becomes active again on its own and its rating is recalculated. Meta notifies you through a WhatsApp Manager notification, an email and the `message_template_status_update` webhook. A pause does not affect the number at first: its other high-quality templates keep going out. But a number that keeps sending low-quality templates may eventually be affected. ## When does error 132015 happen? * **In the middle of a campaign**, when the template collected negative feedback on the first sends. * **On an automated template** (cart reminder, follow-up) going to people who were not expecting it. * **Right after a pause**, if the cause was not addressed: the next pause lasts longer, and the third one is final. ### Where do you see it in Genuka WA? | Channel | What you get | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `POST /api/v1/messages` response | `400`, `"error": "send_template"`, `meta.code: 132015` | | Campaign launch | If Genuka WA already knows the template is paused: a single refusal before the first send, instead of one failure per recipient | | `GET /api/v1/templates/{id}` | `status` and `qualityScore` read live from Meta | | `message_template_status_update` webhook | The pause event, forwarded as Meta sends it | As soon as Genuka WA knows the template is paused, after `GET /api/v1/templates/{id}` or `GET /api/v1/templates?sync=true`, it refuses the send **before** Meta, with the same 132015 code so your error handling reacts the same way: ```json title="400 — POST /api/v1/messages" { "error": "send_template", "message": "Template \"october_promo\" is paused by Meta after negative recipient feedback and cannot be sent. Wait for the pause to lift or edit the template", "meta": { "errorClass": "template", "retryable": false, "code": 132015 } } ``` A disabled template is refused the same way with code 132016. Note that Genuka WA also records as disabled a template that Meta has only **flagged** (the `FLAGGED` webhook event, "at risk of being disabled" according to [Meta](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/message_template_status_update)), even though it can still be sent. Before recreating a template refused with 132016, read it again with `GET /api/v1/templates/{id}`: if Meta still reports it approved, Genuka WA sets it back to `approved` and the refusal goes away. ## How do I fix error 132015? 1. **Stop the automated sends that use this template.** Meta recommends it: they will be rejected anyway while the pause lasts. 2. **Switch to a backup template** for messages that cannot wait, such as an order confirmation. 3. **Choose between waiting and editing.** At Meta, the pause lifts on its own after 3 or 6 hours. Editing the content (`PATCH /api/v1/templates/{id}` with new `components`) sends it back to review: it cannot go out until it is approved again. 4. **Once the pause has lifted, have Genuka WA read the template again** with `GET /api/v1/templates/{id}`, `GET /api/v1/templates?sync=true` or `POST /api/v1/templates/sync`. Only that read guarantees Genuka WA clears its own 132015 refusal: the reinstatement notice received by webhook does not always update its status. Without that read, it can keep answering 132015 after Meta has already reactivated the template. 5. **Fix the cause before resuming**: targeting, frequency, relevance of the message. Otherwise the next pause lasts longer, and the third one disables the template. Meta also lets you unpause a paused template manually, from WhatsApp Manager or through its `unpause` API, and requires it for pauses caused by *template pacing*. Genuka WA does not expose that action: use WhatsApp Manager. **curl** ```bash curl https://wa.genuka.com/api/v1/templates/tpl_123 \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" # { "data": { "name": "october_promo", "status": "paused", "qualityScore": { "score": "RED", ... }, ... } } ``` **Node.js** ```ts /** Call before a bulk send: do not start on a template that is paused or in the red. */ export async function templateIsSafe(templateId: string): Promise { const response = await fetch(`https://wa.genuka.com/api/v1/templates/${templateId}`, { headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}` }, }); const { data } = await response.json(); return data.status === "approved" && data.qualityScore?.score !== "RED"; } ``` **Python** ```python import os import requests def template_is_safe(template_id: str) -> bool: """Call before a bulk send: do not start on a template that is paused or in the red.""" response = requests.get( f"https://wa.genuka.com/api/v1/templates/{template_id}", headers={ "Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}", "User-Agent": "acme-crm/1.0 (+https://example.com)", }, timeout=30, ) response.raise_for_status() data = response.json()["data"] return data["status"] == "approved" and (data.get("qualityScore") or {}).get("score") != "RED" ``` ## How do I prevent error 132015? * **Watch the rating before the pause.** At `YELLOW`, Meta warns the template is at risk of being paused; at `RED`, treat the pause as imminent. That is the moment to slow down. * **Only send to people who opted in** to your messages, and at the right time. * **Make every message useful and expected**: an appointment reminder is rarely reported, a generic promotion sent to your whole list often is. * **Keep a backup template** for every critical message, under another name. ## Related error codes * [131048](https://wa.genuka.com/en/docs/errors/131048): no longer a template but **the number** is restricted for low quality. * [132001](https://wa.genuka.com/en/docs/errors/132001): the template cannot be found in that language, or is not approved. * [131049](https://wa.genuka.com/en/docs/errors/131049): a marketing template held back for one specific recipient. ## FAQ ### How long does a template pause last? 3 hours the first time, 6 hours the second. The third time, the template is disabled (error 132016\) and you need to create a new one. Once the pause has lifted, read the template again with `GET /api/v1/templates/{id}` so Genuka WA stops refusing it. ### Are send attempts during the pause billed? No. Meta states that you are not charged for attempting to send a paused template, and that the attempt does not count against your messaging limit. It is simply rejected. ### Does editing the template lift the pause? Not right away: editing sends it back to review, and it cannot be sent until it is approved again. It helps when the content is the cause, not to save time. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Template pausing](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/template-pausing) * [Meta — Template quality rating](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/template-quality) * [Meta — message\_template\_status\_update webhook](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/message_template_status_update) --- # WhatsApp error 132069: Flow throttled — cause and fix URL: https://wa.genuka.com/en/docs/errors/132069 Language: English > WhatsApp error 132069: the Flow is throttled to 10 sends per hour because its endpoint is unhealthy. How to fix the endpoint and get back to Published. WhatsApp error 132069 means your Flow is in the "throttled" state: WhatsApp detected that its endpoint is unhealthy, through latency, errors or downtime, and limits sending to 10 messages per hour. The cause is on your server. Fix the endpoint; the Flow goes back to published on its own once the metrics recover. ## What does error 132069 mean? > "Flow is in throttled state and 10 messages using this flow were already sent in the last hour." > — [Meta, Cloud API error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes#other-errors) Its neighbouring code describes the next stage: > "Flow is in blocked state." > — Meta, code 132068 For both, Meta's instruction is the same: correct the Flow. These states only apply to Flows that call your endpoint for their data ([Meta, Flow health and monitoring](https://developers.facebook.com/documentation/business-messaging/whatsapp/flows/guides/healthmonitoring)). | Flow state | Restriction | Effect for users | | ------------------ | ----------------------------- | ------------------------------------------ | | Published | None | None | | Throttled (132069) | 10 new Flow messages per hour | Flows already received still open and work | | Blocked (132068) | No sending at all | Flows already sent no longer open | ## When does error 132069 happen? WhatsApp monitors three metrics of a Flow's endpoint: the **error rate** (endpoint or client side), the **latency** and the **availability**. When one of them deteriorates significantly, the Flow first moves to *Throttled*, then to *Blocked* if things get worse. Meta sends several alert webhooks before it gets there. Typical causes: * an endpoint that takes several seconds to answer, because it queries a slow database or API; * an endpoint that does not answer health check requests; * a deployment that breaks decryption or returns errors in a row; * a server that cannot be reached from the internet. ### Where do you see it in Genuka WA? | Channel | What you get | | -------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `POST /api/v1/messages` response | `400`, `"error": "send_template"`, `meta.code: 132069` (or `132068`) | | `GET /api/v1/flows/{id}` | `data.status` read from Meta on every call: `published`, `throttled` or `blocked`, with `validationErrors` | | `flow.status_changed` webhook | The `flows` field events Meta sends, forwarded to your endpoint | Genuka WA classifies 132xxx codes as `template`: sending the same message again changes nothing until the endpoint is fixed. ## How do I fix error 132069? 1. **Read the alerts you received** for this Flow: they say which of the three metrics degraded. 2. **For a latency alert**, speed up the endpoint. Meta recommends answering in under one second: cache, preload, move slow calls off the request path. 3. **For an availability alert**, check that the endpoint is continuously reachable from the internet and that it answers health check requests. The library's `createFlowEndpoint` handler answers them automatically: see [Flows in the library](https://wa.genuka.com/sdk/flows). 4. **For an error rate alert**, go through the errors listed in the alert with Meta's Flows error codes reference. 5. **Wait for the automatic recovery.** WhatsApp detects the fix and moves the Flow from *Blocked* to *Throttled*, then from *Throttled* to *Published*. Follow it with `GET /api/v1/flows/{id}`. **curl** ```bash curl https://wa.genuka.com/api/v1/flows/flw_1 \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" # { "data": { "id": "flw_1", "name": "book_appointment", "status": "throttled", ... }, "validationErrors": [] } ``` **Node.js** ```ts /** Only send a Flow when it is published; otherwise use another path. */ export async function flowIsSendable(flowId: string): Promise { const response = await fetch(`https://wa.genuka.com/api/v1/flows/${flowId}`, { headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}` }, }); const { data } = await response.json(); // "throttled": 10 sends per hour at most. "blocked": none. return data.status === "published"; } ``` ## How do I prevent error 132069? * **Measure your endpoint's latency** under real load, not only in development. * **Monitor the endpoint like a production service**: availability, error rate, response time, with an alert that fires before WhatsApp reacts. * **Receive the `flow.status_changed` events** to know when a Flow changes state without waiting for a send to fail. * **Plan a fallback path** (web link, regular conversation) for when the Flow is not `published`. ## Related error codes * [131047](https://wa.genuka.com/en/docs/errors/131047): a Flow sent as an interactive message outside the 24-hour window is refused; you then need a template with a Flow button. * [132001](https://wa.genuka.com/en/docs/errors/132001): the template carrying the Flow button cannot be found in that language, or is not approved. ## FAQ ### Can my users still fill in a throttled Flow? Yes. In the *Throttled* state, Flows already received open and work; only sending new messages is limited to 10 per hour. In the *Blocked* state, however, even Flows already sent no longer open. ### Are Flows without an endpoint affected? No. Meta states that health monitoring only applies to Flows that use data from your endpoint. ### Do I need to republish the Flow after fixing it? No. WhatsApp automatically detects that the endpoint has recovered and moves the Flow back up, from *Blocked* to *Throttled* and then to *Published*. ## Sources * [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) * [Meta — Flow health and monitoring](https://developers.facebook.com/documentation/business-messaging/whatsapp/flows/guides/healthmonitoring) --- # WhatsApp API comparisons: Twilio, 360dialog, WATI URL: https://wa.genuka.com/en/docs/compare Language: English > Genuka WA vs Twilio, 360dialog, WATI and unofficial WhatsApp APIs: pricing model, Meta fees, coexistence, webhooks and risks, side by side. Genuka WA is a REST API for the official WhatsApp Business Platform: Meta bills your messages with no markup from Genuka, and Genuka charges a subscription per WhatsApp number. These comparisons set it against Twilio, 360dialog and WATI, which also run on the official platform, and against unofficial APIs such as Baileys or WAHA. *Last updated October 8, 2026* ## Which comparison should I read? | You are weighing it against | What mostly changes | Comparison | | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | Twilio | Twilio adds $0.005 to every message, inbound or outbound, on top of Meta's fees ([Twilio](https://www.twilio.com/en-us/whatsapp/pricing)). Genuka WA charges nothing per message. | [Genuka WA vs Twilio](https://wa.genuka.com/en/docs/compare/twilio) | | 360dialog | 360dialog is a Business Solution Provider ([360dialog](https://docs.360dialog.com/docs/get-started/about)) charging a license from €49 per number per month, with no markup on Meta's fees ([360dialog](https://www.360dialog.com/pricing)). Genuka is a Meta Tech Provider. | [Genuka WA vs 360dialog](https://wa.genuka.com/en/docs/compare/360dialog) | | WATI | WATI is a tool for teams: shared inbox, chatbots and campaigns ([WATI](https://www.wati.io/)). Genuka WA is an API for developers. | [Genuka WA vs WATI](https://wa.genuka.com/en/docs/compare/wati) | | Baileys, WAHA and other unofficial APIs | They drive an ordinary WhatsApp account the way WhatsApp Web does, outside the official platform ([Baileys](https://github.com/WhiskeySockets/Baileys), [WAHA](https://waha.devlike.pro/)). | [Official vs unofficial APIs](https://wa.genuka.com/en/docs/compare/unofficial-whatsapp-apis) | ## How are these comparisons written? Every claim about a competitor links to its public source, read on the date shown at the top of the page. Prices change: check them with the provider before you decide. Facts about Genuka WA come from its code and its [pricing](https://wa.genuka.com/en#pricing), also published in Markdown at [/pricing.md](https://wa.genuka.com/pricing.md). ## FAQ ### Is Genuka WA a BSP? No. Genuka is a Meta Tech Provider. A Solution Partner (BSP) has a credit line and bills its clients for usage itself; a client onboarded by a Tech Provider adds its own payment method, and Meta bills it for usage directly ([Meta, Solution Partners and Tech Providers](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview)). ### Is an unofficial WhatsApp API risky? Yes. WhatsApp's Terms of Service forbid accessing the service through unauthorized automated means, as well as bulk messaging and auto-messaging ([WhatsApp, Terms of Service](https://www.whatsapp.com/legal/terms-of-service)). Baileys itself states that it is not affiliated with or endorsed by WhatsApp ([Baileys](https://github.com/WhiskeySockets/Baileys)). The details are in [Official vs unofficial APIs](https://wa.genuka.com/en/docs/compare/unofficial-whatsapp-apis). ### Does Genuka WA suit a team with no developer? Partly. The dashboard lets you connect a number, manage templates, read conversations and reply to them. Automated sends and campaigns, on the other hand, go through the API: Genuka WA has no campaign console, while Genuka Core ([genuka.com](https://genuka.com)) offers one. Someone has to write the integration, or hand it to an [AI agent](https://wa.genuka.com/en/docs/guides/ai-agents). For no-code chatbots, look at a tool such as WATI instead, compared in [Genuka WA vs WATI](https://wa.genuka.com/en/docs/compare/wati). ### How much does Genuka WA cost? A subscription per WhatsApp number, depending on the plan: see [pricing](https://wa.genuka.com/en#pricing). Messages are billed by Meta to your own WhatsApp Business account, with no markup from Genuka; the guide [WhatsApp message prices in Africa](https://wa.genuka.com/en/docs/guides/whatsapp-pricing-africa) breaks those rates down. ## Sources Read on October 8, 2026. * Twilio — [WhatsApp pricing](https://www.twilio.com/en-us/whatsapp/pricing) * 360dialog — [About](https://docs.360dialog.com/docs/get-started/about) * 360dialog — [Pricing](https://www.360dialog.com/pricing) * WATI — [Home page](https://www.wati.io/) * Baileys — [GitHub repository and README](https://github.com/WhiskeySockets/Baileys) * WAHA — [Overview](https://waha.devlike.pro/) * WhatsApp — [Terms of Service](https://www.whatsapp.com/legal/terms-of-service) * Meta — [Solution Partners and Tech Providers](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview) --- # Genuka WA vs Twilio for the WhatsApp API URL: https://wa.genuka.com/en/docs/compare/twilio Language: English > Genuka WA vs Twilio for the WhatsApp Business API, side by side - per-message fees or a flat subscription, Mobile Money, coexistence, webhooks and SDKs. Genuka WA and Twilio both send through Meta's official WhatsApp Business Platform. What differs is the business model. Twilio adds $0.005 to every inbound and outbound message on top of Meta's fees. Genuka WA charges a flat subscription per WhatsApp number, adds nothing per message, and accepts Mobile Money in Central and West Africa. *Last updated October 8, 2026* > [!NOTE] > **How this page was written** > > Every Twilio fact below comes from Twilio's own pricing page and documentation, read on the date > above and listed under [Sources](#sources). Where we could not confirm something, we point you > to their site instead of guessing. Prices change: check both pricing pages before you decide. ## Genuka WA vs Twilio at a glance | | Genuka WA | Twilio | | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | Official WhatsApp Business Platform | Yes. Genuka is a Meta Tech Provider; messages go through Meta's Cloud API on your own WhatsApp Business Account (WABA) | Yes, through Twilio's Programmable Messaging API | | Onboarding a number | Meta Embedded Signup from your dashboard; from the Scale tier, a connect link you share with your clients | Self Sign-up through Meta Embedded Signup, from an upgraded (paid) Twilio account | | WABAs per account | As many as your plan covers: each business keeps its own WABA, all reachable with one API key | One WABA per Twilio account or subaccount (one subaccount per extra WABA) | | Keep a number that is on the WhatsApp Business app | Yes (Meta coexistence) | Their self sign-up guide asks you to delete the WhatsApp or WhatsApp Business app account first | | Pricing model | Subscription per WhatsApp number, with a monthly allowance of outbound messages | Pay as you go, per message | | Provider fee per message | None | $0.005 per inbound or outbound message, plus $0.001 per failed message | | Meta's fees | Billed by Meta to your WABA, no markup from Genuka | Passed through by Twilio on your Twilio bill, on top of its own fee | | Monthly platform fee | From 5,000 FCFA or $19 per number (Starter, monthly billing) | None | | Paying the provider | Mobile Money (XAF, XOF) or card (EUR, USD) | Credit card; PayPal in some regions; wire transfer or ACH for accounts invoiced on an annual commitment | | Docs languages | French and English (SDK guides in English) | English | | Webhooks | JSON, signed with HMAC-SHA256 plus a timestamp (`X-Genuka-Signature`), 5 retries, replay from the dashboard | Status callbacks signed with HMAC-SHA1 (`X-Twilio-Signature`) | | SDKs | TypeScript (`@genuka/whatsapp`), OpenAPI 3.1 spec at `/openapi.json` | C#/.NET, Java, Node.js, PHP, Python, Ruby, Go | | AI-agent tooling | MCP server (`@genuka/whatsapp-mcp`, being published to npm), `/llms.txt`, Markdown version of every docs page | `@twilio-alpha/mcp`, an alpha MCP server exposing Twilio's public APIs | | Testing before go-live | 7-day free trial on your own number, no card required. No sandbox | Twilio Sandbox for WhatsApp | | Other channels | WhatsApp only | SMS, MMS, RCS and WhatsApp in one API | | Africa focus | Prices in FCFA, Mobile Money in Cameroon, Côte d'Ivoire, Senegal, Benin, Burkina Faso, the Republic of the Congo and Gabon | Global platform, prices in USD | ## How much does WhatsApp cost on Twilio vs Genuka WA? Both bills have two layers: what Meta charges for the messages, and what the provider charges for its service. Meta's layer is the same whichever provider you choose. The provider's layer is where the models differ. **Twilio** charges $0.005 for every WhatsApp message, inbound or outbound, plus $0.001 for each message that ends in a `failed` status, with no monthly fee. Meta's fee for each message is added to the same Twilio bill. Twilio mentions that volume discounts may apply. **Genuka WA** charges a subscription per number. The base price covers your first number and each extra number costs less as the tier rises. Each tier includes a monthly allowance of outbound messages per paid number, pooled across the account: on Growth, each number added to the plan ($8 or 2,200 FCFA a month) adds 5,000 messages to the allowance, whether or not it is connected. Inbound messages are never counted or charged. Genuka never bills a message: Meta bills your WABA directly, with no markup. Provider cost for sending from **one number**, monthly billing, Meta's fees excluded. The Genuka WA column shows the cheapest setup for each volume: | Outbound messages per month | Twilio | Genuka WA | | --------------------------- | ------ | ------------------------------------------- | | 500 | $2.50 | $19 or 5,000 FCFA (Starter) | | 5,000 | $25 | $49 or 10,000 FCFA (Growth) | | 10,000 | $50 | $57 or 12,200 FCFA (Growth, 2 paid numbers) | | 15,000 | $75 | $65 or 14,400 FCFA (Growth, 3 paid numbers) | | 30,000 | $150 | $89 or 21,000 FCFA (Growth, 6 paid numbers) | Past the allowance, the API refuses sends (402 `plan_limit_messages`) until you add numbers to the plan, move up a tier or reach the next period; Twilio simply bills each extra message. Two things move these numbers. Every customer reply also costs $0.005 on Twilio, so a two-way conversation doubles the Twilio column; on Genuka WA replies are free. And yearly billing lowers Genuka WA's base price to $15, $39 and $119 a month (3,000, 8,000 and 25,000 FCFA). The full grid, including extra numbers, is on the [pricing page](https://wa.genuka.com/en#pricing) and, in Markdown, at [`/pricing.md`](https://wa.genuka.com/pricing.md). > [!WARNING] > **For low-volume one-way notifications, Twilio is cheaper** > > If you mostly send notifications nobody answers and you pay in USD, Twilio's per-message fee > stays below our price up to about 13,000 messages a month; above that, Genuka WA costs less. > When customers reply, that threshold drops to around 5,000 outbound messages. Genuka WA also > wins when you need FCFA pricing and Mobile Money, or when you run several client businesses under > one account. ### What does Meta charge, whichever provider you pick? Since July 1, 2025, Meta charges per delivered template message, by category (marketing, utility, authentication) and by the recipient's country code. Since **October 1, 2026**, Meta also charges service messages (the free-form replies you send inside the 24-hour customer service window) at the same rate as utility and authentication messages in that market, and utility templates sent inside that window are charged too. Messages sent within the 72-hour free entry point window opened by a Click to WhatsApp ad stay free. None of that depends on the provider. What changes is who sends you the bill: Twilio passes Meta's fee through on your Twilio invoice; with Genuka WA, Meta bills your WABA on the payment method you add in WhatsApp Manager. Until one is added, template sends fail with Meta error `131042`. ## How do I connect a WhatsApp number on each platform? On **Twilio**, you upgrade your account, then run Self Sign-up: log in with Facebook, create or pick a WABA, and verify the number by SMS or voice call. All senders on a Twilio account (or subaccount) must sit in the same WABA; to manage several, Twilio has you create one subaccount per client. If you are a software vendor onboarding *your* clients, Twilio's Tech Provider Program asks you to create your own Meta app, complete business verification and pass Meta's App Review; Twilio says these first steps usually take 3 to 4 weeks. On **Genuka WA**, Genuka already is the Tech Provider, so there is no Meta app to create and no App Review to pass: 1. ### Create an account Sign up, and your workspace opens on a 7-day free trial with one number. No card is required. 2. ### Connect the number through Embedded Signup Open **Numbers**, then **Add a number**: Meta's Embedded Signup runs right in your dashboard. On Scale and Enterprise, you can instead copy your **Connect link** and send it to the business that owns the number; add `?ref=` to it to know which client connected which number. See [Connecting a number](https://wa.genuka.com/en/docs/onboarding). 3. ### Add a payment method on the WABA, then send Meta bills the WABA directly, so its owner adds a payment method in WhatsApp Manager. Then create an API key and send your first message: see the [quickstart](https://wa.genuka.com/en/docs/quickstart). Whichever provider you choose, Meta's own limits apply to the business behind the number: a new business portfolio starts at 250 unique recipients per moving 24-hour period, outside the customer service window, and verifying the business with Meta is one of the ways to raise it to 2,000. ## Can I keep using the WhatsApp Business app on the same number? On Genuka WA, yes. Meta's coexistence mode lets a number already registered on the WhatsApp Business app connect to the API while the app keeps working for one-to-one chats, and history stays in sync. Meta's conditions: app version 2.24.17 or later, history synced within 24 hours of onboarding, and a fixed throughput of 20 messages per second for that number. Only a number registered on the regular consumer WhatsApp app has to be freed first. Step by step: [Coexistence](https://wa.genuka.com/en/docs/guides/coexistence). Twilio's self sign-up guide, as of today, tells you to delete the WhatsApp or WhatsApp Business app account on the number before registering it. If coexistence matters to you, check Twilio's current documentation for any other path. ## How do webhooks and SDKs compare? **Webhooks.** Twilio posts status callbacks to your URL and signs each request with `X-Twilio-Signature`, an HMAC-SHA1 computed from your auth token, the URL and the request parameters. Genuka WA posts one JSON event per delivery, with Meta's raw value untouched inside, signed with `X-Genuka-Signature`: an HMAC-SHA256 of the timestamp and the raw body, so a captured request cannot be replayed later. Failed deliveries are retried 5 times over about 8.5 hours, and any delivery can be replayed from the dashboard. Details in [Webhooks](https://wa.genuka.com/en/docs/webhooks). **SDKs.** Twilio maintains server-side libraries in seven languages. Genuka WA ships one, [`@genuka/whatsapp`](https://wa.genuka.com/en/docs/library) for TypeScript, which validates payloads before the network call and maps Meta's error codes to decisions. From any other language you call the REST API directly, or generate a client from the OpenAPI spec at `/openapi.json`. **AI agents.** Twilio publishes `@twilio-alpha/mcp`, an alpha MCP server that exposes its public APIs. Genuka WA provides an MCP server, `@genuka/whatsapp-mcp` (being published to npm), a `/llms.txt` index, and a Markdown version of every docs page (add `.mdx` to its URL). See [AI agents](https://wa.genuka.com/en/docs/guides/ai-agents). ## What changes in my code if I move from Twilio? Twilio sends a WhatsApp template as a form-encoded request with a `whatsapp:` prefix on each number and a Content SID for the template: ```bash title="Twilio (from Twilio's docs)" curl -X POST "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/Messages.json" \ --data-urlencode "ContentSid=HXXXXXXXXX" \ --data-urlencode "To=whatsapp:+18551234567" \ --data-urlencode "From=whatsapp:+15005550006" \ --data-urlencode "ContentVariables=$CONTENT_VARIABLES_OBJ" \ --data-urlencode "MessagingServiceSid=MGXXXXXXXX" \ -u $TWILIO_API_KEY:$TWILIO_API_SECRET ``` On Genuka WA, the same send is a JSON body. The sender is a `connectionId` (list yours with `GET /api/v1/connections`), and the template is referenced by its name and language: **curl** ```bash curl -X POST https://wa.genuka.com/api/v1/messages \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "to": "+237690000001", "template": { "name": "order_shipped", "language": "en_US", "variables": ["Alice", "#1024"] } }' ``` **Node.js** ```ts const res = await fetch("https://wa.genuka.com/api/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ connectionId: "con_1", to: "+237690000001", template: { name: "order_shipped", language: "en_US", variables: ["Alice", "#1024"] }, }), }); const { data } = await res.json(); // { messageId: "wamid.…" } ``` **Python** ```python import os import requests res = requests.post( "https://wa.genuka.com/api/v1/messages", headers={"Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}"}, json={ "connectionId": "con_1", "to": "+237690000001", "template": {"name": "order_shipped", "language": "en_US", "variables": ["Alice", "#1024"]}, }, timeout=10, ) res.raise_for_status() print(res.json()["data"]["messageId"]) ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("GENUKA_WA_API_KEY"), "Content-Type: application/json", "User-Agent: acme-crm/1.0", ], CURLOPT_POSTFIELDS => json_encode([ "connectionId" => "con_1", "to" => "+237690000001", "template" => ["name" => "order_shipped", "language" => "en_US", "variables" => ["Alice", "#1024"]], ]), ]); $response = json_decode(curl_exec($ch), true); echo $response["data"]["messageId"]; ``` | On Twilio | On Genuka WA | | ------------------------------------- | ---------------------------------------------- | | `From=whatsapp:+…` | `connectionId`, from `GET /api/v1/connections` | | `To=whatsapp:+…` | `to`, in international format with the `+` | | `ContentSid` + `ContentVariables` | `template: { name, language, variables }` | | Basic auth with an API key and secret | `Authorization: Bearer pk_live_…` | | `X-Twilio-Signature` on callbacks | `X-Genuka-Signature` on webhooks | Every message type (text, media, buttons, lists, templates) goes through the same endpoint: see [Sending messages](https://wa.genuka.com/en/docs/messages) and the [API reference](https://wa.genuka.com/en/docs/api). ## When should you choose Twilio instead? * **You need SMS, MMS or RCS next to WhatsApp.** Twilio covers them in the same Messaging API; Genuka WA is WhatsApp only. * **Your traffic is one-way and modest.** Notifications nobody replies to cost less on Twilio's pay-as-you-go pricing than on our plans billed in USD, up to about 13,000 messages a month. * **You want a sandbox before connecting a real number.** Twilio has one; Genuka WA does not. * **Your team works in Java, C#, Go or Ruby and wants an official SDK.** Twilio maintains one per language; we ship TypeScript and an OpenAPI spec. * **You want Meta's fees on the same invoice as your provider's.** Twilio passes them through; with Genuka WA, Meta bills your WABA separately. ## When is Genuka WA the better fit? * **You bill in FCFA or pay by Mobile Money.** Plans are priced in XAF and XOF and paid from a Mobile Money wallet; card is available in EUR and USD. * **Your customers write back.** Inbound messages cost nothing on our side, so conversations do not double your bill. * **The number already runs on the WhatsApp Business app.** Coexistence keeps the app working. * **You onboard numbers for other businesses.** From the Scale tier, share one connect link, tag each client with `?ref=`, and manage every WABA from one API key, without becoming a Tech Provider yourself. * **You want French documentation.** All API docs exist in French and English (the TypeScript SDK guides are English only). ## FAQ ### Is Genuka WA cheaper than Twilio for WhatsApp? It depends on volume and replies. For one-way notifications billed in USD, Twilio's $0.005 per message costs less than our plans up to about 13,000 messages a month; above that, Genuka WA costs less, by adding numbers to the Growth plan (each one adds 5,000 messages to the allowance for $8). Once customers reply, Twilio charges each reply as well: at 5,000 outbound messages a month with as many replies, both cost about $50, and above about 5,700 Genuka WA costs less (10,000 messages: $57 against $100). Meta's own fees are the same on both. ### Do Twilio and Genuka WA both use the official WhatsApp API? Yes. Both send through Meta's WhatsApp Business Platform. Genuka WA is a Meta Tech Provider: you connect your own WABA through Meta's Embedded Signup and Meta bills that WABA directly. ### Can I pay for WhatsApp with Mobile Money? You can pay your Genuka WA subscription by Mobile Money in XAF or XOF. Twilio's help center lists credit cards, PayPal in some regions, and wire transfer or ACH for monthly invoicing, which is reserved for commitments of at least $12,000 a year; it says local payment methods are not supported yet. In both cases, Meta's own message fees are not paid by Mobile Money: Twilio adds them to its invoice, and with Genuka WA Meta charges the payment method on your WABA. ### Does Twilio charge for incoming WhatsApp messages? Yes. Twilio's pricing page lists $0.005 per message, inbound or outbound. Genuka WA does not charge for incoming messages, and they do not count against your plan's allowance. ### Do I still need Meta business verification? Not to become a provider: Genuka holds the Tech Provider status. Meta still applies its limits to your business, though. A new business starts at 250 unique recipients per moving 24-hour period, outside the customer service window, and verifying it is one way to raise that limit. To send one-time codes (authentication templates), you do need a verified business: without it, creating those templates is refused. See [WhatsApp API without Meta verification](https://wa.genuka.com/en/docs/guides/without-meta-verification). ## Sources Read on October 8, 2026. * Twilio, WhatsApp pricing: [https://www.twilio.com/en-us/whatsapp/pricing](https://www.twilio.com/en-us/whatsapp/pricing) * Twilio, WhatsApp Self Sign-up: [https://www.twilio.com/docs/whatsapp/self-sign-up](https://www.twilio.com/docs/whatsapp/self-sign-up) * Twilio, Overview of the WhatsApp Business Platform with Twilio: [https://www.twilio.com/docs/whatsapp/api](https://www.twilio.com/docs/whatsapp/api) * Twilio, WhatsApp docs and Sandbox: [https://www.twilio.com/docs/whatsapp](https://www.twilio.com/docs/whatsapp) * Twilio, Tech Provider Program: [https://www.twilio.com/docs/whatsapp/isv/tech-provider-program](https://www.twilio.com/docs/whatsapp/isv/tech-provider-program) * Twilio, Tech Provider Program integration guide: [https://www.twilio.com/docs/whatsapp/isv/tech-provider-program/integration-guide](https://www.twilio.com/docs/whatsapp/isv/tech-provider-program/integration-guide) * Twilio, Send WhatsApp notification messages with templates: [https://www.twilio.com/docs/whatsapp/tutorial/send-whatsapp-notification-messages-templates](https://www.twilio.com/docs/whatsapp/tutorial/send-whatsapp-notification-messages-templates) * Twilio, Messaging overview: [https://www.twilio.com/docs/messaging](https://www.twilio.com/docs/messaging) * Twilio, Server-side SDKs: [https://www.twilio.com/docs/libraries](https://www.twilio.com/docs/libraries) * Twilio, Webhooks security: [https://www.twilio.com/docs/usage/webhooks/webhooks-security](https://www.twilio.com/docs/usage/webhooks/webhooks-security) * Twilio, Register WhatsApp senders for ISVs (subaccounts): [https://www.twilio.com/docs/whatsapp/isv/register-senders](https://www.twilio.com/docs/whatsapp/isv/register-senders) * Twilio Help Center, About Payment Types: [https://support.twilio.com/hc/en-us/articles/49507358452635-About-Payment-Types](https://support.twilio.com/hc/en-us/articles/49507358452635-About-Payment-Types) * Twilio Help Center, Can I Use PayPal to Pay Twilio?: [https://support.twilio.com/hc/en-us/articles/223183368-Can-I-Use-PayPal-to-Pay-Twilio](https://support.twilio.com/hc/en-us/articles/223183368-Can-I-Use-PayPal-to-Pay-Twilio) * Twilio Alpha, MCP server: [https://github.com/twilio-labs/mcp](https://github.com/twilio-labs/mcp) * Meta, WhatsApp Business Platform pricing: [https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing) * Meta, Pricing updates for service and utility messages: [https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing/non-template-messages](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing/non-template-messages) * Meta, Solution Partners and Tech Providers: [https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview) * Meta, Onboarding customers as a Tech Provider: [https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-customers-as-a-tech-provider](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-customers-as-a-tech-provider) * Meta, Onboarding WhatsApp Business app users (coexistence): [https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users) * Meta, Messaging limits: [https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits) * Meta, Error codes: [https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) --- # Genuka WA vs 360dialog: WhatsApp API compared URL: https://wa.genuka.com/en/docs/compare/360dialog Language: English > Genuka WA vs 360dialog for the WhatsApp Business API - price per number, how Meta fees are paid, Mobile Money, coexistence, webhooks and MCP tooling. Genuka WA and 360dialog both connect you to Meta's official WhatsApp Business Platform with no markup on Meta's rate cards (with one exception at 360dialog, covered below). 360dialog is a Meta Business Solution Provider charging from €49 per number per month, paid by card. Genuka WA is a Meta Tech Provider charging from 5,000 FCFA per number payable by Mobile Money, or €19 by card. *Last updated October 8, 2026* > [!NOTE] > **How this page was written** > > Every 360dialog fact below comes from 360dialog's own pricing page and documentation, read on the > date above and listed under [Sources](#sources). Where we could not confirm something, we point > you to their site instead of guessing. Prices change: check both pricing pages before you decide. ## Genuka WA vs 360dialog at a glance | | Genuka WA | 360dialog | | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Status with Meta | Meta Tech Provider | Official WhatsApp Business Solution Provider (BSP) and Premier Meta Partner | | Official WhatsApp Business Platform | Yes, Meta's Cloud API on your own WhatsApp Business Account (WABA) | Yes, Meta's Cloud API | | Onboarding a number | Meta Embedded Signup from your dashboard; from the Scale tier, a connect link you share with your clients | Embedded Signup hosted by 360dialog | | Keep a number that is on the WhatsApp Business app | Yes (Meta coexistence) | Yes (Meta coexistence) | | Pricing model | Subscription per WhatsApp number, with a monthly allowance of outbound messages | Licence per number (channel) per month; their pricing page lists no message allowance | | Monthly price, one number | From €19, $19 or 5,000 FCFA (Starter) | From €49 or $59 (Regular); €99 or $119 (Premium); higher tiers on their pricing page | | Markup on Meta's fees | None | None on Meta's rate cards, except a 7% surcharge on marketing content sent through the standard messages endpoint instead of Meta's Marketing Messages API; their `/messages` endpoint routes to the marketing endpoint automatically when applicable | | How Meta's fees are paid | Meta bills your WABA directly, on the payment method you add in WhatsApp Manager | From a prepaid balance per number, topped up in the 360dialog Hub; outbound messages pause when it reaches zero | | Paying the provider | Mobile Money (XAF, XOF) or card (EUR, USD) | Visa, Mastercard or American Express card, with a 4% processing fee on licence invoices | | Billing currencies | XAF, XOF, EUR, USD | EUR or USD for the subscription | | API style | Simplified JSON (`connectionId`, `to`, one content field), with Meta's `components` accepted as-is | Meta's Cloud API payloads, with a `D360-API-KEY` header | | Webhooks | Genuka envelope around Meta's raw value, HMAC-SHA256 with timestamp (`X-Genuka-Signature`), 5 retries, replay from the dashboard | Meta's payloads, signed with `x-360dialog-signature`; answer 200 within 5 seconds; Meta retries for up to 7 days | | SDKs | TypeScript (`@genuka/whatsapp`), OpenAPI 3.1 spec at `/openapi.json` | No official SDK in their docs index; REST examples | | AI-agent tooling | MCP server (`@genuka/whatsapp-mcp`, being published to npm), `/llms.txt`, Markdown version of every docs page | Hosted MCP server for account, template, webhook and balance management; `llms.txt` and Markdown docs | | Testing before go-live | 7-day free trial on your own number, no card required. No sandbox | Sandbox: 200 messages at most, to one fixed recipient (your number), 3 predefined templates | | Support | Standard (Starter), priority (Growth), dedicated (Scale), contractual SLA (Enterprise) | 24/7 human support, first response under 4 hours (Regular) or 30 minutes (Premium) | | Docs languages | French and English (SDK guides in English) | English | | Africa focus | Prices in FCFA, Mobile Money in Cameroon, Côte d'Ivoire, Senegal, Benin, Burkina Faso, the Republic of the Congo and Gabon | Global platform, subscription in EUR or USD | ## How much does each cost per WhatsApp number? Both models put a flat fee on the number and leave the message cost to Meta. The difference is in what the fee includes. 360dialog's Regular licence is €49 per number per month, and their pricing page sets no message allowance on it. Genuka WA's tiers cost less at the start and include a monthly allowance of outbound messages per paid number, pooled across the account: each number added to the plan adds its allowance, whether or not it is connected. Incoming messages are never counted. Provider cost for sending from **one number**, monthly billing, Meta's fees excluded. The Genuka WA column shows the cheapest setup for each volume: | Outbound messages per month | 360dialog (Regular) | Genuka WA | | --------------------------- | ------------------- | ------------------------------------------- | | 500 | €49 | €19 or 5,000 FCFA (Starter) | | 5,000 | €49 | €49 or 10,000 FCFA (Growth) | | 10,000 | €49 | €57 or 12,200 FCFA (Growth, 2 paid numbers) | | 30,000 | €49 | €89 or 21,000 FCFA (Growth, 6 paid numbers) | Provider cost for **five numbers**, monthly billing, Meta's fees excluded: | | 360dialog (Regular) | Genuka WA | | ----- | ------------------- | ------------------------------------------------------------ | | Price | 5 × €49 = €245 | Starter: €19 + 4 × €9 = €55, or 15,000 FCFA (2,500 messages) | | | | Growth: €49 + 4 × €8 = €81, or 18,800 FCFA (25,000 messages) | On Genuka WA the allowance is pooled: five Growth numbers share 25,000 outbound messages a month. Past the allowance, the API refuses sends (402 `plan_limit_messages`) until you add numbers, move up a tier or reach the next period. Yearly billing lowers our base price to €15, €39 and €119 a month (3,000, 8,000 and 25,000 FCFA). The full grid is on the [pricing page](https://wa.genuka.com/en#pricing) and, in Markdown, at [`/pricing.md`](https://wa.genuka.com/pricing.md). > [!WARNING] > **One high-volume number? 360dialog can be cheaper** > > Above 5,000 outbound messages a month on a single number, paid in EUR, 360dialog's €49 Regular > licence costs less than our plans (€57 for 10,000 messages, €89 for 30,000). Genuka WA is cheaper > when you run several numbers, send moderate volumes per number, or pay in FCFA. ### Who pays Meta, and how? The rates are Meta's in both cases: since July 1, 2025 Meta charges per delivered template, by category and by the recipient's country code, and since **October 1, 2026** it also charges service messages (free-form replies inside the 24-hour window) and utility templates sent inside that window. Messages in the 72-hour free entry point window opened by a Click to WhatsApp ad stay free. The payment path differs. A BSP such as 360dialog can extend Meta's credit line to its clients: 360dialog deducts Meta's charges from a prepaid balance per number, which you top up by card in its Hub, manually or automatically below a threshold. A Tech Provider has no credit line, so with Genuka WA the business adds its own payment method to its WABA in WhatsApp Manager and Meta bills it directly. Until one is added, template sends fail with Meta error `131042`. ## How do I connect a number on each platform? Both use Meta's Embedded Signup: the business logs in with Facebook, picks or creates its WABA, and registers the number. Both support Meta's coexistence mode, so a number already on the WhatsApp Business app keeps the app for one-to-one chats. Meta's conditions apply everywhere: app version 2.24.17 or later, history synced within 24 hours, and a fixed 20 messages per second for that number. On Genuka WA, step by step: [Coexistence](https://wa.genuka.com/en/docs/guides/coexistence). For platforms that onboard numbers on behalf of clients, 360dialog sells a separate Partner Platform starting at €250 a month plus a fee per channel. On Genuka WA, this comes with the Scale tier (€149 or 30,000 FCFA a month, then €6 or 1,800 FCFA per extra number), with no separate partner plan: the dashboard gives you a connect link to send to your clients, a list of your clients and their sub-accounts. Tag each client with `?ref=` and find their number with `GET /api/v1/connections?externalRef=…`. White-label branding is included from Growth. See [Connecting a number](https://wa.genuka.com/en/docs/onboarding). ## How different are the API and the webhooks? 360dialog exposes Meta's Cloud API almost as-is: you post Meta's payload to `https://waba-v2.360dialog.io/messages` with your `D360-API-KEY`. Genuka WA wraps the same Cloud API in a shorter body, so the common cases stay readable, and keeps two doors open to Meta's raw format: a `template.components` array is forwarded untouched, and a `raw` field takes any Cloud API message fragment. Template definitions also use Meta's `components` array verbatim, so the templates you already wrote carry over. See [Sending messages](https://wa.genuka.com/en/docs/messages) and the [API reference](https://wa.genuka.com/en/docs/api). On webhooks, 360dialog forwards Meta's payloads, signed with `x-360dialog-signature`; you must answer `200` within 5 seconds, and Meta retries failed deliveries for up to 7 days. Genuka WA posts one event per request, with Meta's raw value inside an envelope that names the business and the number, signed with `X-Genuka-Signature` (HMAC-SHA256 over the timestamp and the raw body). You get 10 seconds to answer, 5 retries over about 8.5 hours, and a replay button in the dashboard. See [Webhooks](https://wa.genuka.com/en/docs/webhooks). Both also serve AI agents. 360dialog hosts an MCP server that manages accounts, channels, templates, webhooks and balance, and publishes its docs as `llms.txt` and Markdown. Genuka WA provides an MCP server, `@genuka/whatsapp-mcp` (being published to npm), a `/llms.txt` index and a Markdown version of every docs page: see [AI agents](https://wa.genuka.com/en/docs/guides/ai-agents). ## What changes in my code if I move from 360dialog? A template send on 360dialog, from their documentation: ```bash title="360dialog (from 360dialog's docs)" curl -X POST https://waba-v2.360dialog.io/messages \ -H "D360-API-KEY: {{api-key}}" \ -H "Content-Type: application/json" \ -d '{ "messaging_product": "whatsapp", "to": "PHONE_NUMBER", "type": "template", "template": { "name": "hello_world", "language": { "code": "en_US" } } }' ``` The same send on Genuka WA. Your API key covers every number on the account, so the body names the sender with a `connectionId` (list yours with `GET /api/v1/connections`): **curl** ```bash curl -X POST https://wa.genuka.com/api/v1/messages \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "con_1", "to": "+237690000001", "template": { "name": "hello_world", "language": "en_US" } }' ``` **Node.js** ```ts const res = await fetch("https://wa.genuka.com/api/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ connectionId: "con_1", to: "+237690000001", template: { name: "hello_world", language: "en_US" }, }), }); const { data } = await res.json(); // { messageId: "wamid.…" } ``` **Python** ```python import os import requests res = requests.post( "https://wa.genuka.com/api/v1/messages", headers={"Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}"}, json={ "connectionId": "con_1", "to": "+237690000001", "template": {"name": "hello_world", "language": "en_US"}, }, timeout=10, ) res.raise_for_status() print(res.json()["data"]["messageId"]) ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("GENUKA_WA_API_KEY"), "Content-Type: application/json", "User-Agent: acme-crm/1.0", ], CURLOPT_POSTFIELDS => json_encode([ "connectionId" => "con_1", "to" => "+237690000001", "template" => ["name" => "hello_world", "language" => "en_US"], ]), ]); $response = json_decode(curl_exec($ch), true); echo $response["data"]["messageId"]; ``` | On 360dialog | On Genuka WA | | -------------------------------- | ------------------------------------------------------------------------------ | | `D360-API-KEY` header | `Authorization: Bearer pk_live_…` | | `messaging_product`, `type` | Implied by the content field you send | | `template.language.code` | `template.language` | | `template.components` | `template.variables` for body values, or `template.components` forwarded as-is | | Any other Cloud API message type | A dedicated field (`text`, `image`, `buttons`…) or `raw` | | `x-360dialog-signature` | `X-Genuka-Signature` | ## When should you choose 360dialog instead? * **One number, high volume, paid in EUR.** A flat €49 licence with no message allowance beats our plans above 5,000 outbound messages a month on that number. * **You want a sandbox.** 360dialog has one (200 messages to your own number); Genuka WA does not. * **You want Meta's fees paid through your provider.** 360dialog's prepaid balance means no payment method to set up in WhatsApp Manager. * **You want 24/7 human support with a first-response SLA** on every number, from the entry plan. ## When is Genuka WA the better fit? * **You pay in FCFA, by Mobile Money.** Plans are priced in XAF and XOF; card is available in EUR and USD. * **You run several numbers.** Extra numbers cost €6 to €9 a month (1,800 to 2,500 FCFA) depending on the tier, instead of a full licence each. * **You onboard numbers for your clients.** From the Scale tier, the connect link, `?ref=` tagging and client sub-accounts are included, with no separate €250-a-month partner plan. * **You would rather not manage a prepaid balance.** Meta bills the WABA's own payment method, so there is no per-number balance to top up. * **You want French documentation.** All API docs exist in French and English (the TypeScript SDK guides are English only). ## FAQ ### Does 360dialog mark up Meta's WhatsApp fees? Not on Meta's rate cards. Their documentation adds one exception: marketing content sent through the standard messages endpoint, instead of Meta's Marketing Messages API, carries a 7% surcharge on Meta's rate. Their API reference adds that the `/messages` endpoint routes to the marketing endpoint automatically when applicable. Genuka WA never bills messages at all: Meta charges your WABA directly. ### Is Genuka WA cheaper than 360dialog? For one to a few numbers with moderate volume, yes: our tiers start at €19 and extra numbers cost €6 to €9 a month. For a single number sending more than 5,000 outbound messages a month, paid in EUR, 360dialog's €49 Regular licence costs less. ### Can I keep the WhatsApp Business app with either provider? Yes. Both support Meta's coexistence mode: the app keeps working for one-to-one chats while the API sends at scale, with history kept in sync. Only a number on regular consumer WhatsApp has to be freed first. ### Can I pay with Mobile Money? On Genuka WA, yes, in XAF or XOF. 360dialog's payment documentation lists Visa, Mastercard and American Express cards. Meta's own message fees are paid to 360dialog's prepaid balance by card, or, with Genuka WA, to Meta through the payment method on your WABA. ### Will my 360dialog code work on Genuka WA? Mostly with small changes. Template definitions use Meta's `components` array on both, and Genuka WA accepts raw Cloud API fragments through `template.components` and `raw`. What changes is the authentication header, the base URL and the `connectionId` that names the sending number. ## Sources Read on October 8, 2026. * 360dialog, pricing: [https://www.360dialog.com/pricing](https://www.360dialog.com/pricing) * 360dialog, Pricing (docs): [https://docs.360dialog.com/docs/get-started/pricing](https://docs.360dialog.com/docs/get-started/pricing) * 360dialog, Payments: [https://docs.360dialog.com/docs/get-started/payments](https://docs.360dialog.com/docs/get-started/payments) * 360dialog, Funds: [https://docs.360dialog.com/docs/hub/funds](https://docs.360dialog.com/docs/hub/funds) * 360dialog, About: [https://docs.360dialog.com/docs/get-started/about](https://docs.360dialog.com/docs/get-started/about) * 360dialog, Coexistence: [https://docs.360dialog.com/docs/hub/embedded-signup/whatsapp-coexistence](https://docs.360dialog.com/docs/hub/embedded-signup/whatsapp-coexistence) * 360dialog, Sandbox: [https://docs.360dialog.com/docs/get-started/sandbox](https://docs.360dialog.com/docs/get-started/sandbox) * 360dialog, 360Dialog MCP: [https://docs.360dialog.com/docs/get-started/mcp](https://docs.360dialog.com/docs/get-started/mcp) * 360dialog, Send and receive messages: [https://docs.360dialog.com/docs/guides/send-and-receive-messages](https://docs.360dialog.com/docs/guides/send-and-receive-messages) * 360dialog, Messaging API reference (messages): [https://docs.360dialog.com/docs/messaging-api/api-reference/messages](https://docs.360dialog.com/docs/messaging-api/api-reference/messages) * 360dialog, Webhooks: [https://docs.360dialog.com/docs/messaging/webhook](https://docs.360dialog.com/docs/messaging/webhook) * 360dialog, Meta Business Agent webhooks (signature): [https://docs.360dialog.com/docs/mba/webhooks](https://docs.360dialog.com/docs/mba/webhooks) * 360dialog, documentation index: [https://docs.360dialog.com/llms.txt](https://docs.360dialog.com/llms.txt) * Meta, WhatsApp Business Platform pricing: [https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing) * Meta, Pricing updates for service and utility messages: [https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing/non-template-messages](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing/non-template-messages) * Meta, Solution Partners and Tech Providers: [https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview) * Meta, Onboarding customers as a Tech Provider: [https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-customers-as-a-tech-provider](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-customers-as-a-tech-provider) * Meta, Onboarding WhatsApp Business app users (coexistence): [https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users) * Meta, Error codes: [https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes) --- # Genuka WA vs WATI: WhatsApp API or team inbox tool URL: https://wa.genuka.com/en/docs/compare/wati Language: English > Genuka WA or WATI for the WhatsApp Business API: a REST API for developers or a team inbox with chatbots. Pricing, Meta fees and webhooks compared. Genuka WA and WATI both run on Meta's official WhatsApp Business Platform, but they serve different jobs. WATI is a ready-made tool for a team: shared inbox, no-code chatbots, campaigns. Genuka WA is a REST API for developers, billed per WhatsApp number, where Meta bills your messages directly with no markup from Genuka. *Last updated October 8, 2026* > [!NOTE] > **How this page was written** > > Everything about WATI comes from its pricing page, its per-message rate cards and its API > documentation, read on the date above and listed under [Sources](#sources). WATI shows different > prices depending on the visitor's country: we quote the ones shown for France and for Cameroon. > Genuka publishes Genuka WA; check both price lists before you choose. ## Genuka WA and WATI at a glance | | Genuka WA | WATI | | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | Built for | Developers and software vendors who build WhatsApp into their product | Support, sales and marketing teams who want a ready-made tool | | Official WhatsApp Business Platform | Yes. Genuka is a Meta Tech Provider; messages go through Meta's Cloud API on your own WhatsApp Business Account (WABA) | Yes. WATI presents itself as an Official Meta Business Partner | | What you use day to day | A REST API and webhooks. The dashboard connects numbers, manages templates, tracks sends and lets you read and answer conversations | A team inbox (WhatsApp, Instagram, Messenger, TikTok…), no-code chatbots and a campaign tool | | WhatsApp numbers | Up to 5, 15 or 100 depending on the tier; every extra number costs less than the first | One channel on Growth and Pro; multiple numbers on Business | | Users included | 1, 5 or 15 depending on the tier | 3 on Growth, with no way to add more; 5 on Pro and Business, then paid | | API access | Every tier | Every plan, capped at 10,000 calls a month on Growth, 200,000 on Pro, 20 million on Business | | Webhooks | Every tier. Signed with timestamped HMAC-SHA256, 5 retries, replay from the dashboard | None on Growth, "limited" on Pro, "extensive" on Business | | Meta message fees | Billed by Meta straight to your WABA, with no Genuka markup | Paid to WATI as prepaid credits, at WATI's rate card | | Cheapest monthly subscription | 5,000 FCFA, €19 or $19 (Starter) | €69 in France, $49 in Cameroon (Growth) | | Payment methods | Mobile Money (FCFA) or card | Cards that support international purchases; bank transfer on annual plans | | Keep the WhatsApp Business app on the number | Yes (Meta coexistence) | Yes, WATI advertises syncing with the WhatsApp Business app | | Free trial | 7 days, no card | 7 days, no setup fee | | Chatbots and AI | No chatbot builder: your logic plugs into the webhooks. An MCP server (`@genuka/whatsapp-mcp`) lets an AI agent call the API: see [WhatsApp API for AI agents](https://wa.genuka.com/en/docs/guides/ai-agents) | No-code chatbots, AI Co-pilot credits included, Astra AI agents as a paid add-on | | Running numbers for many businesses | A connection link to send to each client, white-label from Growth, client sub-accounts from Scale | Partner and reseller programs (see their site) | ## Developer API or team tool: what is the difference? **WATI assumes people answer the customers.** Its pricing page describes a team inbox (assign, track, follow up, tag, report), no-code chatbots, campaigns with open and read rates, the WhatsApp catalog and, depending on the plan, Shopify, HubSpot or Salesforce integrations. The API comes on top: 10,000 calls a month and no webhooks on Growth. **Genuka WA assumes your code talks to the customers.** Your backend sends an order confirmation, a one-time code or a campaign over HTTP. Replies and delivery receipts arrive on your signed webhooks, and your application decides what to do with them. The dashboard lets you read conversations and answer them, but it is not a support tool: no agent assignment, no routing rules, no chatbot builder, no campaign console. > [!NOTE] > **Want a no-code campaign console?** > > Genuka WA exposes campaigns as an API primitive (`/api/v1/campaigns`: a template, a recipient > list, a status per recipient). If your team wants to compose campaigns in a UI, Genuka Core > ([genuka.com](https://genuka.com)) offers one. ## How much do WATI and Genuka WA cost? Each bill has two layers: the provider's subscription, and the messages Meta charges for. ### How much is the subscription? WATI prices depend on the visitor's country. On October 8, 2026, its pricing page showed, for France: | WATI plan | Monthly | Annual, per month | What is included | | --------- | ------- | ----------------- | ----------------------------------------------------------------------------- | | Growth | €69 | €59 | 1 channel, 3 users, 15,000 broadcasts a month, 10,000 API calls, no webhooks | | Pro | €149 | €119 | 1 channel, 5 users, unlimited broadcasts, 200,000 API calls, limited webhooks | | Business | €349 | €279 | Multiple numbers, 5 users, 20 million API calls, extensive webhooks | For Cameroon, WATI applies a regional price list in US dollars: Growth at $49 ($39 a month billed annually), Pro at $99 ($79), Business at $249 ($199). Message charges come on top on every plan. Genuka WA bills the WhatsApp number: a base price that covers the first number, then a cheaper rate for every additional one, lower the higher the tier. | Genuka WA tier | Monthly | Annual, per month | Extra number, per month | What is included | | -------------- | ------------------- | ------------------- | ----------------------- | ---------------------------------------------------------------------------------------------- | | Starter | 5,000 FCFA or $19 | 3,000 FCFA or $15 | 2,500 FCFA or $9 | Up to 5 numbers, 500 outbound messages per number per billing period, 1 user, API and webhooks | | Growth | 10,000 FCFA or $49 | 8,000 FCFA or $39 | 2,200 FCFA or $8 | Up to 15 numbers, 5,000 messages per number, 5 users, white-label, priority support | | Scale | 30,000 FCFA or $149 | 25,000 FCFA or $119 | 1,800 FCFA or $6 | Up to 100 numbers, 30,000 messages per number, 15 users, client sub-accounts | | Enterprise | Quote | Quote | Quote | Custom volume, terms and SLA | Euro prices use the same figures as dollar prices. The outbound message allowance is a subscription limit, not metering: once it is used up, the API answers `402 plan_limit_messages` until the next period, or until you add numbers or move up a tier. The full grid is on the [pricing page](https://wa.genuka.com/en#pricing) and in [Plans & billing](https://wa.genuka.com/en/docs/billing). ### Who bills the WhatsApp messages? Meta charges for templates by category (marketing, utility, authentication) and by the recipient's country. Since **October 1, 2026**, Meta also charges for service messages (the free-form replies sent inside the 24-hour window) at the country's utility rate, and utility templates sent inside that window are now billed too. WATI and Trengo state that the first 1,000 service messages of each number stay free every month. Messages sent inside the 72-hour free entry point window opened by a Click to WhatsApp ad stay free. That layer is the same with every provider. What changes is the path the bill takes. **With Genuka WA**, Meta bills your WABA directly, on the card you add to your WhatsApp Business account. Genuka never touches those fees and adds nothing to them. **With WATI**, you top up credits with WATI, and each message's cost is deducted from that balance. The price applied is WATI's rate card, which varies with your account's region and your plan. An excerpt from its two US-dollar rate cards dated October 1, 2026, for the Growth and Pro plans: | Recipient country | Category | USD "EAST" rate card (accounts in Cameroon) | USD "WEST" rate card (accounts in the United States) | | ----------------- | --------- | ------------------------------------------- | ---------------------------------------------------- | | Cameroon | Marketing | $0.0281 | $0.0360 | | Cameroon | Utility | $0.0052 | $0.0072 | | France | Marketing | $0.1074 | $0.1374 | | France | Utility | $0.0390 | $0.0540 | WATI states that an additional discount applies on Business. To put these figures in context, compare them with Meta's official rate card for the same country (linked under [Sources](#sources)): that Meta rate, and nothing else, is what a Genuka WA account pays. ### Which plan fits which need? Subscription only, billed monthly, Meta fees excluded: | Need | Genuka WA | WATI (France prices) | | ---------------------------------------------- | --------------------------- | ---------------------------------- | | One number, API sends and webhooks, one person | Starter: €19 | Pro: €149 (Growth has no webhooks) | | One number, a team of 3 to 5, webhooks | Growth: €49 | Pro: €149 | | Three numbers | Starter: €19 + 2 × €9 = €37 | Business: from €349 | > [!WARNING] > **It is not the same product** > > WATI's price includes the team inbox, chatbots, integrations and AI credits, none of which Genuka > WA provides. If your team needs them and nobody can build them, these rows do not compare one to > one. And Genuka WA's allowance matters: Starter includes 500 outbound messages per number per > billing period, Growth 5,000. ## How do the APIs and webhooks compare? | | Genuka WA | WATI | | ------------------------ | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | Base URL | `https://wa.genuka.com/api/v1` | `https://live-mt-server.wati.io`, with `/api/ext/v3/…` for v3 and your tenant ID in the path for v1 | | Authentication | `Authorization: Bearer pk_live_…`, key created in the dashboard | `Authorization: Bearer `, token created in the WATI dashboard | | Send a template | `POST /api/v1/messages` with a `template` field | `POST /api/ext/v3/messageTemplates/send` | | Send a free-form message | `POST /api/v1/messages` with `text`, `image`, `buttons`… | `POST /api/ext/v3/conversations/messages/text` | | Campaign | `POST /api/v1/campaigns`, then `POST /api/v1/campaigns/{id}/launch` | The same template endpoint, with `broadcast_name` and up to 10,000 recipients | | Webhooks | `X-Genuka-Signature` header: HMAC-SHA256 of the timestamp and the raw body | Set up in the WATI dashboard; the webhook introduction page describes no signature | | Retries | 5, after 1 min, 5 min, 30 min, 2 h, then 6 h; manual replay from the log | Up to 144, every 10 minutes | | Specification | OpenAPI 3.1 at [`/openapi.json`](https://wa.genuka.com/openapi.json) | An OpenAPI 3.0 definition per endpoint in the docs | ### How do I send a template with Genuka WA? One Meta-approved template, sent to one recipient. `variables` fills the body's `{{1}}`, `{{2}}` parameters, in order: **curl** ```bash curl -X POST https://wa.genuka.com/api/v1/messages \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "cnx_...", "to": "+237699001122", "template": { "name": "order_shipped", "language": "en_US", "variables": ["Awa", "#1042"] } }' # { "data": { "messageId": "wamid.HBg..." } } ``` **Node.js** ```js const res = await fetch("https://wa.genuka.com/api/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ connectionId: "cnx_...", to: "+237699001122", template: { name: "order_shipped", language: "en_US", variables: ["Awa", "#1042"], }, }), }); const body = await res.json(); if (!res.ok) { // { "error": "code", "message"?: "…" } throw new Error(`${res.status} ${body.error}${body.message ? `: ${body.message}` : ""}`); } console.log(body.data.messageId); // "wamid.HBg..." ``` **Python** ```python import os import requests res = requests.post( "https://wa.genuka.com/api/v1/messages", headers={"Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}"}, json={ "connectionId": "cnx_...", "to": "+237699001122", "template": { "name": "order_shipped", "language": "en_US", "variables": ["Awa", "#1042"], }, }, timeout=10, ) res.raise_for_status() print(res.json()["data"]["messageId"]) # "wamid.HBg..." ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("GENUKA_WA_API_KEY"), "Content-Type: application/json", "User-Agent: my-shop/1.0", ], CURLOPT_POSTFIELDS => json_encode([ "connectionId" => "cnx_...", "to" => "+237699001122", "template" => [ "name" => "order_shipped", "language" => "en_US", "variables" => ["Awa", "#1042"], ], ]), ]); $body = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) { $msg = $body['message'] ?? ''; throw new RuntimeException(trim("$status {$body['error']} $msg")); } echo $body["data"]["messageId"]; // "wamid.HBg..." ``` A `200` means Meta accepted the message, not that it was delivered: the final status arrives on your [webhooks](https://wa.genuka.com/en/docs/webhooks). Read the `connectionId` with `GET /api/v1/connections`. ### What changes in my code if I leave WATI? | On WATI | On Genuka WA | | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `POST /api/ext/v3/messageTemplates/send` with `template_name`, `broadcast_name` and `recipients[]` | One recipient: `POST /api/v1/messages` with `template.name`, `template.language` and `template.variables`. A list: `POST /api/v1/campaigns` (`connectionId`, `templateId` from `GET /api/v1/templates`, `name`, `recipients[].to`, `recipients[].variables`), then `/launch` | | `POST /api/ext/v3/conversations/messages/text` with `target` and `text` | `POST /api/v1/messages` with `to` and `text` | | `channel`, the channel's name or number | `connectionId`, read with `GET /api/v1/connections` | | Number without `+`, for example `14155552671` | International number with `+`, for example `+237699001122` | | Named `custom_params` (`name` and `value`) | Positional `variables`, or `bodyNamed` for a template with named parameters | | Separate WATI events (`messageReceived`, `templateMessageSent_v2`, `sentMessageDELIVERED_v2`…) | One `messages` event for inbound messages and statuses, with Meta's raw value in `data` and a signature to verify | The full reference is in the [API documentation](https://wa.genuka.com/en/docs/api) and in [Sending messages](https://wa.genuka.com/en/docs/messages). For the receiving side, see [receive replies and statuses via webhook](https://wa.genuka.com/en/docs/guides/receive-messages-webhooks). ## When should I choose WATI over Genuka WA? * Your agents answer customers by hand and need assignment, team routing and activity reports. * You want to build chatbots without writing code, or gather WhatsApp, Instagram and Messenger in one inbox. * You rely on ready-made integrations (Shopify, HubSpot, Salesforce) rather than your own. * Nobody on the team will write code against an API. ## When is Genuka WA the better choice? * You are building WhatsApp into your product: order notifications, login codes, campaigns triggered by your backend. See the guides on [order notifications](https://wa.genuka.com/en/docs/guides/order-notifications), [sending a WhatsApp OTP from Node.js](https://wa.genuka.com/en/docs/guides/whatsapp-otp-nodejs) and [sending a campaign through the API](https://wa.genuka.com/en/docs/guides/campaigns-api). * You run the numbers of several client businesses: each one connects through your link, every extra number costs less, and one API key covers all your clients. See [Connecting a number](https://wa.genuka.com/en/docs/onboarding). * You want Meta to bill your messages directly, with no middleman and no markup. * You want signed webhooks from the first tier. * You pay in FCFA, by Mobile Money. ## FAQ ### Is Genuka WA cheaper than WATI? On the subscription, yes for an integration use case: €19 a month for one number with API and webhooks, against €149 for the first WATI plan that includes webhooks (prices shown for France). But WATI includes a team inbox and chatbots. On messages, a Genuka WA account pays Meta's rate; on WATI, WATI's rate card applies. ### Do WATI and Genuka WA both use the official WhatsApp API? Yes. WATI presents itself as an Official Meta Business Partner; Genuka is a Meta Tech Provider. Either way, messages go through Meta's Cloud API under the same rules: Meta-approved templates outside the 24-hour window, recipient opt-in, messaging limits. ### Can I keep my number if I leave WATI? Meta provides a procedure to migrate a number from one partner to another, which keeps the display name, quality rating, messaging limits and approved high-quality templates. It has prerequisites: two-step verification turned off on the number, a valid payment method on the source WABA, and a business verified by Meta. Check your case with us before you switch anything off. ### Does Genuka WA have an inbox? Yes, a simple one in the dashboard: you read the messages your numbers receive and answer them. It has no agent assignment, routing or chatbot. To automate, your webhooks receive every inbound message. ### Can I pay with Mobile Money? On Genuka WA, yes: the subscription can be paid by Mobile Money in FCFA (Genuka Pay in Cameroon, pawaPay in the other CFA franc countries we cover) or by card. WATI's FAQ lists payment by cards that support international purchases, and by bank transfer on annual plans. ## Sources Read on October 8, 2026. * [WATI, pricing](https://www.wati.io/pricing/) * [WATI, per-message rate card in USD, "EAST", Growth and Pro plans, October 1, 2026](https://drive.google.com/file/d/1lwkuIkAnjLJexinXCcziL7Ld2H5sTP98/view) * [WATI, per-message rate card in USD, "WEST", Growth and Pro plans, October 1, 2026](https://drive.google.com/file/d/1dt4sTvjXW12gXOk9y8UL-rxFlBAtNKWX/view) * [WATI, Understanding Wati's pricing structure](https://support.wati.io/en/articles/11462993-understanding-wati-s-pricing-structure) * [WATI, home page](https://www.wati.io/) * [WATI, API authentication](https://docs.wati.io/reference/authentication) * [WATI, send template messages (API v3)](https://docs.wati.io/reference/messagetemplate_sendtemplatemessages) * [WATI, send a text message (API v3)](https://docs.wati.io/reference/conversations_sendtext) * [WATI, webhooks introduction](https://docs.wati.io/reference/introduction-1) * [WATI, tracking template message status with webhooks](https://support.wati.io/en/articles/11463225-how-to-track-template-message-delivery-and-message-status-using-wati-webhooks) * [Trengo, WhatsApp pricing changes from October 1, 2026](https://help.trengo.com/article/whatsapp-pricing-changes-from-1-october-2026) * [Meta, WhatsApp Business Platform pricing and official rate cards](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing) * [Meta, billing of service messages and utility templates from October 1, 2026](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing/non-template-messages) * [Meta, get started as a Tech Provider](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/get-started-for-tech-providers) * [Meta, onboarding WhatsApp Business app users (coexistence)](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users) * [Meta, migrating a phone number between partners via Embedded Signup](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/support/migrating-phone-numbers-among-solution-partners-via-embedded-signup) --- # Official WhatsApp API vs unofficial APIs (WAHA, Baileys…) URL: https://wa.genuka.com/en/docs/compare/unofficial-whatsapp-apis Language: English > Whapi.Cloud, WAHA, Green API, Baileys: how unofficial WhatsApp APIs work, their ban and reliability risks, and what changes with the official API. An unofficial WhatsApp API (Whapi.Cloud, WAHA, Green API, Evolution API in Baileys mode, Baileys, whatsapp-web.js) drives a WhatsApp account the way WhatsApp Web does, after you scan a QR code. It is quick and needs no approval from Meta, but it breaks WhatsApp's Terms of Service and the number can be banned. The official API goes through Meta's Cloud API. *Last updated October 8, 2026* > [!NOTE] > **How this page was written** > > Genuka sells access to the official API, so we have a stake in this comparison. To keep it fair, > every claim about a tool comes from that tool's own documentation, and every WhatsApp rule from > WhatsApp's own texts, read on the date above and listed under [Sources](#sources). We found no > published ban rate, from WhatsApp or from these projects, so we quote none. ## How does an unofficial WhatsApp API work? WhatsApp lets you link secondary devices to an account: that is how WhatsApp Web works. An unofficial API poses as one of those devices. You scan a QR code from **Linked devices** on your phone, or enter a pairing code, and the tool sends and receives the account's messages the way a browser would. There are two techniques: * **Drive the real WhatsApp Web in a browser.** whatsapp-web.js and WAHA's WEBJS and WPP engines launch Chromium with Puppeteer and call WhatsApp Web's internal functions. * **Speak the WhatsApp Web protocol directly.** Baileys, and WAHA's NOWEB and GOWS engines, open a WebSocket connection with no browser. On top of that, some projects add an HTTP API and webhooks: WAHA and Evolution API run on your own server, while Green API and Whapi.Cloud are hosted services that keep the session for you. | Tool | Type | How it connects to WhatsApp | Listed price | What the project says | | --------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | Baileys | TypeScript library, MIT license | WebSocket, QR code or pairing code | Free | "not affiliated \[…] with WhatsApp"; "Do not spam people with this" | | whatsapp-web.js | Node.js library, Apache 2.0 license | WhatsApp Web driven by Puppeteer | Free | "it is not guaranteed you will not be blocked by using this method" | | WAHA | Self-hosted HTTP server (Docker), Apache 2.0 license | Browser (WEBJS, WPP) or WebSocket (NOWEB, GOWS), QR code | Free; optional $5 a month support tier | "WhatsApp does not allow bots or unofficial clients on their platform" | | Evolution API | Self-hosted REST server, Apache 2.0 license with extra conditions (usage notice required, otherwise a commercial license) | Your choice: Baileys (WhatsApp Web) or Meta's official Cloud API | Free | Baileys mode "may have limitations compared to official APIs" | | Green API | Hosted service | QR code through Linked devices; the phone stays charged and online, or is hosted by them | Developer free, Business $12 a month, Chatbot $24 a month | "The decision to block an account is made by WhatsApp" | | Whapi.Cloud | Hosted service | Linked-device session, QR code or pairing code | Limited free Sandbox; Developer Premium at $29 a month per number (shown struck through from $40) | "Any automation can carry a risk of WhatsApp restrictions" | Evolution API is only unofficial in Baileys mode: its Cloud API connector goes through Meta's platform, under the same rules as Genuka WA. ## Why do developers pick an unofficial API? * **Start in minutes.** A QR code scan is enough: no Meta business portfolio, no Embedded Signup, no template to get approved. * **Message anyone, any time.** Whapi.Cloud advertises "No templates or approvals". On the official API, a message sent outside the 24-hour window must be a template approved by Meta. * **A flat price, no per-message fees.** Whapi.Cloud charges "no per-message, per-conversation, or per-request fees"; WAHA is free, with no limit on messages. On the official API, Meta charges for templates and, since October 1, 2026, for free-form replies beyond 1,000 free service messages per number per month. * **Groups, Channels and Status.** Whapi.Cloud and WAHA expose them. Genuka WA's API does not handle groups. * **Keep your phone.** The session is added next to your existing devices; the app keeps working as before. These are real reasons. The question is what they cost. ## What are the risks of an unofficial WhatsApp API? ### What do WhatsApp's Terms of Service say? WhatsApp's Terms of Service forbid communications that involve "bulk messaging, auto-messaging, auto-dialing, and the like", as well as any "non-personal use of our Services unless otherwise authorized by us". They also forbid exploiting the service "through automated or other means" in unauthorized ways, and to "create software or APIs that function substantially the same as our Services and offer them for use by third parties in an unauthorized manner". When the terms are breached, WhatsApp may "modify, suspend, or terminate your access". The projects say so themselves: whatsapp-web.js and WAHA write that WhatsApp does not allow bots or unofficial clients, and Baileys' maintainers state they do not condone any use that violates WhatsApp's Terms of Service. ### Can my number get banned? Yes, and none of these tools claims otherwise. Green API writes that the decision to block an account belongs to WhatsApp and does not depend on its service. It lists signs of automation, complaints from recipients and the response ratio, recommends warming up a new number for at least 10 days, and advises messaging no more than 200 customers a day. Whapi.Cloud acknowledges that any automation carries a risk of restrictions. On the Baileys repository, users still report accounts banned after bulk sends (issue opened October 7, 2026). A ban does not only cost you the integration: it costs you the number your customers know, along with its conversations. ### Why is having no templates a problem? On the official platform, the WhatsApp Business policy requires the recipient's consent: "You may only contact people on WhatsApp if: (a) they have given you their mobile phone number \[…]; and (b) you have received opt-in permission \[…]". Outside the 24-hour window, only a template reviewed by Meta can go out, and each business portfolio has a messaging limit, shared by its numbers, that rises when its messages are high quality and at least half of the limit is used. On an unofficial session, none of these guardrails exists: nothing stops you from messaging a cold list. And recipient complaints are among the blocking causes Green API lists. ### Can a session disconnect? Yes. Whapi.Cloud says you usually need to use WhatsApp on the phone at least once every 14 days to keep the session active, and that WhatsApp may still reset linked sessions and require re-authorization. Green API reminds you that sending goes through the phone, which must stay charged and online, unless you rent a phone hosted by them. WAHA warns that API responses and webhook payloads differ significantly from one engine to another. For order notifications or login codes, that is a failure point to plan for. ### Who can read your conversations? A linked device receives the account's conversations, not only the ones your integration cares about: Baileys even documents fetching the full history. With a hosted service, that provider holds your number's session. ## When can an unofficial API make sense? These uses stay outside the framework WhatsApp provides. The risk is the same everywhere; what varies is what it costs you. It stays bearable when losing the number costs next to nothing: * **Automating your own account**: personal reminders, archiving your messages, notifications sent to yourself. * **A prototype or a demo**, on a dedicated number you accept to lose, with recipients who know they are testing. * **A low-volume internal tool**, between people who know you and have written to you. As soon as customers, login codes or payments depend on the number, or you message people who did not ask to hear from you, the trade-off flips. ## How is the official API through Genuka WA different? The Cloud API is the path Meta provides for businesses. Genuka is a Meta Tech Provider: you connect your own WhatsApp Business number through Meta's Embedded Signup, then send notifications, one-time codes and campaigns over HTTP, without applying to Meta as a provider yourself. | | Official API, through Genuka WA | Unofficial API | | -------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | Allowed by WhatsApp | Yes, it is the platform built for businesses | No: the Terms of Service forbid unauthorized automated access | | Connecting the number | Meta's Embedded Signup, on your own WhatsApp Business Account (WABA) | QR code scan, like a linked device | | Messaging a customer first | A Meta-approved template | Any message | | Volume | 250 unique recipients per 24 hours for a new business portfolio, then 2,000, 10,000, 100,000 and unlimited | No published cap; Green API advises staying under 200 customers a day | | Throughput | Meta's ceiling of 80 messages per second per number by default, 20 in coexistence | Whatever the session allows | | Groups, Channels, Status | Not supported by Genuka WA | Yes, depending on the tool | | Delivery statuses | Webhooks signed with HMAC-SHA256, retried, replayable | The tool's webhooks | | Cost | Meta's per-message fees, billed to your WABA, plus a Genuka subscription per number | A flat subscription, or free when self-hosted | | Main risk | A rejected template; a lower quality rating if recipients complain | A banned number, a dropped session | What you give up, honestly: * You cannot message first without a Meta-approved template. * Volume starts low: 250 unique recipients per 24 hours. It rises to 2,000 after Meta verifies the business, or after 2,000 messages delivered outside the window within 30 days using high-quality templates, then scales up automatically. See [what works without Meta verification](https://wa.genuka.com/en/docs/guides/without-meta-verification). * Templates have a Meta cost, by category and recipient country. Since October 1, 2026, Meta also charges for the free-form replies sent inside the 24-hour window, beyond 1,000 free service messages per number per month. * Genuka WA does not send to groups, Channels or Status. What you get: * You are inside the framework WhatsApp provides. The remaining risk is quality: messages people do not want lower the number's rating, and Meta can restrict it. * No phone, browser or QR code to keep alive. * Replies and statuses (`sent`, `delivered`, `read`, `failed`) arrive on [signed webhooks](https://wa.genuka.com/en/docs/webhooks), retried 5 times and replayable from the log. * Marketing opt-outs are enforced: Genuka WA refuses a marketing template it knows about when it is aimed at an opted-out contact (`403 recipient_opted_out`), and a marketing campaign skips those contacts (status `skipped`). * Meta bills your messages directly, with no Genuka markup. The subscription starts at 5,000 FCFA or $19 a month per number, with a 7-day trial and no card. See the [pricing page](https://wa.genuka.com/en#pricing). ## How do I move from an unofficial API to the official API? 1. ### Find out which app runs the number * **The number runs on the WhatsApp Business app**: keep it. This is Meta's **coexistence**: the app keeps working one-to-one and the chat history syncs. You need app version 2.24.17 or later, and the history sync must happen within 24 hours. When you connect, Meta unlinks every companion device from the account, the unofficial session included: do not link it again afterwards. * **The number is registered on regular WhatsApp**: it must be freed first, by deleting the WhatsApp account on that number, or you use another number. In coexistence, Meta states that group chats are not synchronized and that the app's broadcast lists are disabled. The details are in the [Coexistence](https://wa.genuka.com/en/docs/guides/coexistence) guide. 2. ### Connect the number to Genuka WA Create an account: onboarding offers to connect a number. After that it is **Numbers** > **Add a number** > **Connect WhatsApp**, which opens Meta's Embedded Signup. The number, its WABA and its templates show up in your workspace. Add a payment method to the WhatsApp Business account: Meta is the one billing the messages. See [Connecting a number](https://wa.genuka.com/en/docs/onboarding). 3. ### Submit your templates Every message you send first (order confirmation, reminder, promotion) becomes a template that Meta must approve: submit them early. One-time codes use the `AUTHENTICATION` category, which is reserved for businesses that passed Meta business verification (or another of Meta's scaling paths): see [send a WhatsApp OTP from Node.js](https://wa.genuka.com/en/docs/guides/whatsapp-otp-nodejs). 4. ### Collect your contacts' consent Only message people who agreed to hear from you on WhatsApp, and stop as soon as they opt out: the opt-out reaches you by webhook. 5. ### Replace the send call and wire up webhooks Create an API key, replace your send call (see below), then register your [webhook](https://wa.genuka.com/en/docs/webhooks) URL and verify the signature before processing anything: see [receive replies and statuses via webhook](https://wa.genuka.com/en/docs/guides/receive-messages-webhooks). ### What changes in the send code? Before, with a WAHA session, a free-form message goes to any number: ```bash curl -X POST https://waha.example.com/api/sendText \ -H "X-Api-Key: $WAHA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "session": "default", "chatId": "237699001122@c.us", "text": "Hi Awa, your order #1042 has shipped." }' ``` With Genuka WA, the same message becomes an approved template, and you only pass its variables: **curl** ```bash curl -X POST https://wa.genuka.com/api/v1/messages \ -H "Authorization: Bearer $GENUKA_WA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "connectionId": "cnx_...", "to": "+237699001122", "template": { "name": "order_shipped", "language": "en_US", "variables": ["Awa", "#1042"] } }' # { "data": { "messageId": "wamid.HBg..." } } ``` **Node.js** ```js const res = await fetch("https://wa.genuka.com/api/v1/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ connectionId: "cnx_...", to: "+237699001122", template: { name: "order_shipped", language: "en_US", variables: ["Awa", "#1042"], }, }), }); const body = await res.json(); if (!res.ok) { // For example 403 recipient_opted_out, 402 plan_limit_messages; `message` is optional throw new Error(`${res.status} ${body.error}${body.message ? `: ${body.message}` : ""}`); } console.log(body.data.messageId); // "wamid.HBg..." ``` **Python** ```python import os import requests res = requests.post( "https://wa.genuka.com/api/v1/messages", headers={"Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}"}, json={ "connectionId": "cnx_...", "to": "+237699001122", "template": { "name": "order_shipped", "language": "en_US", "variables": ["Awa", "#1042"], }, }, timeout=10, ) res.raise_for_status() print(res.json()["data"]["messageId"]) # "wamid.HBg..." ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("GENUKA_WA_API_KEY"), "Content-Type: application/json", "User-Agent: my-shop/1.0", ], CURLOPT_POSTFIELDS => json_encode([ "connectionId" => "cnx_...", "to" => "+237699001122", "template" => [ "name" => "order_shipped", "language" => "en_US", "variables" => ["Awa", "#1042"], ], ]), ]); $body = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) { $msg = $body['message'] ?? ''; throw new RuntimeException(trim("$status {$body['error']} $msg")); } echo $body["data"]["messageId"]; // "wamid.HBg..." ``` To answer a customer who wrote to you within the last 24 hours, swap `template` for `text`: a free-form message is enough. Outside that window the API still answers `200`, with a `warning` field (`outside_service_window`, or `unverified_service_window` when no inbound message from that contact is on record). Meta does not deliver the message, and usually reports it later as a `failed` status with error [`131047`](https://wa.genuka.com/en/docs/errors/131047) on your webhooks. Every message type is described in [Sending messages](https://wa.genuka.com/en/docs/messages). ## FAQ ### Is an unofficial WhatsApp API legal? It breaches WhatsApp's Terms of Service, which WhatsApp enforces by suspending or terminating access to the service. Beyond that, rules such as data protection or direct marketing law in your country apply whatever tool you use. ### Will my number definitely get banned? No, but nobody can promise you otherwise, and we found no published ban rate. The tools themselves name the factors that raise the risk: a new number, bulk sends, recipients who do not reply or who complain. ### Can I keep my number when I move to the official API? Yes if the number runs on the WhatsApp Business app: Meta's coexistence connects it to the Cloud API without touching the app. A number registered on regular WhatsApp must be freed first. Either way, the unofficial session has to go. ### Can the official API post in WhatsApp groups? Yes, to a point: Meta offers a Groups API on the Cloud API, open to Official Business Accounts, with at most 8 participants per group, who join through an invite link. It is not available on a number that also runs the WhatsApp Business app. Genuka WA does not expose it: it sends one-to-one messages, templates and campaigns. In coexistence, the app's group chats are not synchronized. ### How much does the official API cost with Genuka WA? Two layers: Meta bills your paid messages to your WhatsApp Business Account, by category and recipient country, and Genuka charges a subscription per number, from 5,000 FCFA or $19 a month, with no markup on messages. See [Plans & billing](https://wa.genuka.com/en/docs/billing). ## Sources Read on October 8, 2026. * [WhatsApp, Terms of Service](https://www.whatsapp.com/legal/terms-of-service) * [WhatsApp, WhatsApp Business Messaging Policy](https://whatsappbusiness.com/policy/) * [Baileys, GitHub repository and README](https://github.com/WhiskeySockets/Baileys) * [Baileys, issue #2850: accounts banned after bulk sends](https://github.com/WhiskeySockets/Baileys/issues/2850) * [whatsapp-web.js, GitHub repository and README](https://github.com/pedroslopez/whatsapp-web.js) * [WAHA, overview](https://waha.devlike.pro/) * [WAHA, engines](https://waha.devlike.pro/docs/how-to/engines/) * [WAHA, sending messages](https://waha.devlike.pro/docs/how-to/send-messages/) * [Evolution API, GitHub repository and README](https://github.com/EvolutionAPI/evolution-api) * [Green API, overview and pricing](https://green-api.com/en/) * [Green API, before you start](https://green-api.com/en/docs/before-start/) * [Green API, how to protect a number from ban](https://green-api.com/en/docs/faq/how-to-protect-number-from-ban/) * [Whapi.Cloud, overview and FAQ](https://whapi.cloud/) * [Whapi.Cloud, pricing](https://whapi.cloud/price) * [Meta, message templates](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/overview) * [Meta, messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits) * [Meta, about the WhatsApp Business Platform (Cloud API)](https://developers.facebook.com/documentation/business-messaging/whatsapp/about-the-platform) * [Meta, Groups API](https://developers.facebook.com/documentation/business-messaging/whatsapp/groups/) * [Meta, onboarding WhatsApp Business app users (coexistence)](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users) * [Meta, WhatsApp Business Platform pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing) * [Meta, billing of service messages from October 1, 2026](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing/non-template-messages) * [WATI, Understanding Wati's pricing structure (1,000 free service messages per number per month)](https://support.wati.io/en/articles/11462993-understanding-wati-s-pricing-structure) * [Trengo, WhatsApp pricing changes from October 1, 2026](https://help.trengo.com/article/whatsapp-pricing-changes-from-1-october-2026) --- # Sending messages URL: https://wa.genuka.com/sdk/messages Language: English > One send path, one retry policy, one throughput gate — MessageClient, MM Lite routing, and the 24-hour window. The messages module of `@genuka/whatsapp`: one send path, one retry policy, one throughput gate. ```ts import { MetaTransport } from "@genuka/whatsapp"; import { messages } from "@genuka/whatsapp"; import { MessageClient } from "@genuka/whatsapp/messages/send"; const client = new MessageClient({ transport: new MetaTransport({ accessToken }), phoneNumberId: "1234567890", }); const result = await client.send(messages.text("+237699001122", "Bonjour")); result.messageId; // "wamid.HBg…" ``` The same client works against Genuka WA instead of Meta — swap `MetaTransport` for `GenukaTransport` and nothing else changes. Both transports project failures onto the same `WhatsAppError` / `errorClass` taxonomy, which is what makes the retry policy below transport- independent. > `messages/send.ts` is this module's entry point. Once the package root re-exports it, everything > below is reachable from `@genuka/whatsapp` directly; the deep path stays valid either way. *** ## 1. `MessageClient.send()` ```ts send(message: OutboundMessage, context?: SendContext): Promise ``` `message` is whatever `messages.*` built — text, media, interactive, template. It is posted **verbatim**: nothing is added, nothing is stripped, so `biz_opaque_callback_data` set by the builder reaches Meta unchanged and comes back on the status webhook. `MessageSendResult` is Meta's response plus three fields: | Field | Meaning | | ----------------------- | --------------------------------------------------------------------------------------- | | `messageId` | the wamid, hoisted out of `messages[0]` | | `endpoint` | `"messages"` or `"marketing_messages"` — which edge accepted it, **after** any fallback | | `fellBackFromMarketing` | `true` when MM Lite was tried, was unavailable, and `/messages` took over | Failures reach you as `WhatsAppError`, built by the transport, **not re-wrapped**. Branch on `errorClass`, never on the message text: ```ts try { await client.send(message); } catch (error) { if (error instanceof WhatsAppError && error.errorClass === "needs_template") { // 131047: the 24h window closed. Re-engage with an approved template. } } ``` | Situation | Meta code | `errorClass` | | ------------------------------------ | ---------------- | --------------------- | | service window closed | 131047 | `needs_template` | | stale or unreachable media | 131052 | `media` | | throughput / rate limit | 130429, HTTP 429 | `retryable` | | template paused, missing, malformed | 132xxx | `template` | | recipient not on WhatsApp, opted out | 131026, 131050 | `recipient_permanent` | | per-user marketing cap | 131049 | `recipient_throttled` | *** ## 2. MM Lite routing Marketing Messages Lite is a **second send endpoint**, not a second API: same WABA, same number, same approved templates, same payload. Only the URL differs. ```http POST /{PHONE_NUMBER_ID}/messages ← everything POST /{PHONE_NUMBER_ID}/marketing_messages ← MARKETING templates only ``` Routing is driven by the **template category**, which the caller supplies: ```ts await client.send(promo, { category: "MARKETING" }); // → /marketing_messages await client.send(receipt, { category: "UTILITY" }); // → /messages await client.send(otp, { category: "AUTHENTICATION" }); // → /messages await client.send(promo); // → /messages (no category, no guess) ``` ### Why the caller supplies the category The category belongs to the approved template, not to the send payload — it is simply not present in `OutboundMessage`, and Graph never echoes it. Inferring it from the template *name* is the one shortcut worth refusing: `promo_confirmation_v2` is a name, not a contract, and a mis-inferred category puts an OTP on a channel that is allowed to hold it back for hours. Pass `disableMarketingRoute: true` to force the classic endpoint for a send whose timing must not be optimised (an A/B against MM Lite, a time-critical promo drop). ### ⚠️ `accepted` means queued, not sent This is the single most important sentence on this page. On `/messages`, Meta accepts the payload and hands it to the delivery pipeline. On `/marketing_messages`, Meta may **deliberately delay** the message to hit a moment the user is likely to open it, and may **deliberately drop** it against the per-user marketing cap — a legitimate `failed`, not a bug (131049, 131050). So `message_status: "accepted"` is an acknowledgement of receipt by Meta and nothing more. The only source of truth for what happened to a message is the **`messages` status webhook**: `sent` → `delivered` → `read`, or `failed`. Campaign UIs must say *"envoi en cours"*, and every counter must be fed by webhooks, never by send responses. ### Silent fallback MM Lite is not enabled on every account or in every country, and Meta exposes no capability flag to check beforehand — you find out by posting. When the marketing endpoint refuses for an availability reason, the send is retried once on `/messages` and `fellBackFromMarketing: true` is set on the result. "Availability reason" means a 4xx that classifies as `config` (capability/permission missing) or `unknown` (unsupported POST on a non-existent edge). Deliberately excluded: * **message-level classes** (`template`, `validation`, `needs_template`, `recipient_*`, `media`) — the same payload would be rejected identically on `/messages`, so falling back would only double the failure rate; * **credential failures** (codes 0 and 190) — the second call would fail the same way, with `fellBackFromMarketing` hiding the real cause; * **5xx and network failures** — those are `retryable` and belong to the retry policy, not to endpoint selection. Worth persisting `fellBackFromMarketing` on the message row: it is the only trace that this account never had MM Lite in the first place. *** ## 3. Retry policy `decideRetry` is a pure function of `errorClass`. No timers, no network, no clock — which is why it can be tested exhaustively and swapped wholesale. ```ts import { decideRetry, runWithRetry, shouldReschedule } from "@genuka/whatsapp/messages/send"; const outcome = decideRetry(error, attempt); // attempt is 1-based, the one that just failed // { action: "retry", delayMs, invalidateMedia } | { action: "fail", reason } | { action: "reschedule" } ``` | `errorClass` | Behaviour | | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | `retryable` | 3 attempts, exponential backoff + jitter (500 ms → 1 s → …, capped at 30 s) | | `media` | invalidate the cached `media_id`, then **exactly one** more attempt | | `recipient_throttled` | **`reschedule`** — drop from this campaign run, schedule into a later one | | `recipient_permanent`, `template`, `config`, `validation`, `needs_template`, `unknown` | never retried | `unknown` is treated as permanent on purpose: the safe failure mode for a code we do not understand is to stop and get it classified in `errors.ts`. ### Jitter `delay = min(500 × 2^(attempt-1), 30_000)`, then a uniform draw over the top `1 - jitterRatio` of it. At the default `jitterRatio: 0.5`, a nominal 2 s wait becomes a draw in `[1 s, 2 s]` — enough spread to break the thundering herd of a fleet of workers that all took a 429 in the same millisecond, without collapsing the backoff to nothing. ### The runner ```ts const result = await runWithRetry((attempt) => client.send(message), { media: { resolver, assetId, phoneNumberId }, // enables invalidate-then-retry on 131052 onRetry: ({ attempt, delayMs }) => log.warn({ attempt, delayMs }), }); ``` * `sleep` and `random` are injectable, so tests run in microseconds and produce the same numbers every time. The defaults are a real timer and `Math.random`. * On a `media` failure the resolver is invalidated **before** the wait, so a worker that dies mid-backoff has still dropped the poisoned id. * Anything that is not a `WhatsAppError` propagates untouched: a `TypeError` in your own code is a bug, and retrying it three times only makes the stack trace harder to find. * The last failure is rethrown with `errorClass` intact — including `recipient_throttled`, which `shouldReschedule(error)` identifies for the campaign runner. Re-resolving the `media_id` between attempts is the media module's job: `runWithRetry` invalidates the cache, the operation must ask the resolver again on its next call. *** ## 4. Rate limiting Meta caps sends **per business phone number**, per second. Exceeding it does not queue — it fails with 130429. | Number | Cap | | ----------------------------------------------- | ----------------- | | default (registered Cloud API number) | **80 mps** | | upgraded (`throughput.level = HIGH_THROUGHPUT`) | **1 000 mps** | | coexistence (`platform_type = SMB_APP`) | **20 mps**, fixed | ```ts import { RateLimiter, resolveThroughput, estimateDuration } from "@genuka/whatsapp/messages/send"; const mps = resolveThroughput({ platformType: connection.platformType, // "SMB_APP" wins over everything throughputLevel: connection.throughputLevel, }); const limiter = new RateLimiter({ mps }); for (const recipient of recipients) { await limiter.run(() => client.send(build(recipient), { category: "MARKETING" })); } ``` **Coexistence overrides everything.** An `SMB_APP` number reports a throughput level like any other, and that level is meaningless: the 20 mps ceiling comes from the WhatsApp Business app sharing the number, not from the tier. Reading the level first is the bug `resolveThroughput` exists to prevent. ### Sliding, not fixed A fixed-window counter allows twice the cap across a boundary — 80 grants at `12:00:00.999` and 80 more at `12:00:01.001` is 160 in two milliseconds, and Meta counts all 160. `RateLimiter` remembers when the last `mps` grants happened and refuses a slot until the oldest has aged out of the trailing second. It is **single-process** state. A campaign sharded over several workers must divide the cap between them, or move the counter behind shared storage: Meta enforces the ceiling on the number, not on the process. ### Announcing the duration before launch ```ts estimateDuration(100_000, 20); // 4_999_950 ms ≈ 83 minutes limiter.estimateDuration(100_000); // same, using the limiter's own cap ``` `(count - 1) / mps` seconds: the first burst of `mps` leaves immediately, so the figure is the wait until the *last* message goes out. It is a floor — it assumes the loop keeps the pipe saturated and that Meta never throttles — so present it as "at least", never as an ETA. A campaign of 100 000 messages on a coexistence number takes about an hour and a half. The operator has to be told that *before* pressing the button. *** ## 5. Read receipts and typing ```ts await client.markAsRead(inboundWamid); await client.typing(inboundWamid); // marks as read AND shows the bubble ``` Exposed over HTTP as: ```http POST /api/v1/messages/{id}/read { "connectionId": "…", "typing": true } ``` `{id}` is the **inbound wamid** from the `messages` webhook. It can contain `/` and `=`, so URL-encode it into the path. Three rules Meta imposes, and one consequence each: * **Typing only exists coupled to a read receipt.** There is no way to type spontaneously; the call marks a specific inbound message as read and shows the bubble in the same request. * **The bubble auto-dismisses after `limits.CONVERSATION.typingIndicatorSeconds` (25 s)**, or as soon as the reply is sent, whichever comes first. There is no cancel and no refresh — call it when the reply is actually being produced. A bubble that expires with nothing behind it reads worse to the customer than no bubble at all. * **A message can only be marked read for `limits.CONVERSATION.markReadWithinDays` (30 days)** after it arrived. Past that Meta refuses and the blue ticks are lost for good, so a read-receipt backlog is worth draining rather than queueing indefinitely. Marking one message read also marks **every earlier message of that conversation** read: catching up on a thread is one call on its newest inbound message, not one per message. --- # Templates URL: https://wa.genuka.com/sdk/templates Language: English > Building, validating and managing message templates: components, buttons, carousel, limited-time offer and authentication. Build a template, submit it, follow its lifecycle, read its analytics. > **The distinction that trips everyone.** A template *being defined* and a template *being sent* > are two different objects with the same vocabulary. > > | | defining | sending | > | -------- | ------------------------------------------------------------ | ------------------------------------------------------------------ | > | module | `@genuka/whatsapp/templates` | `messages.template()` | > | shape | `{ type: "HEADER", format: "TEXT", text: "Commande {{1}}" }` | `{ type: "header", parameters: [{ type: "text", text: "A-42" }] }` | > | contains | placeholders and *examples* | filled *parameters* | > | lifetime | submitted once, reviewed, reused for months | one message | > > Everything below is the left-hand column. *** ## 1. Building a template ```ts import { defineTemplate, textHeader, body, footer, buttons, quickReply, urlButton, phoneButton, } from "@genuka/whatsapp/templates"; const template = defineTemplate({ name: "commande_expediee", // lowercase, digits, underscores. Nothing else. language: "fr", category: "UTILITY", components: [ textHeader({ text: "Commande {{1}}", examples: ["A-42"] }), body({ text: "Bonjour {{1}}, votre colis part demain.", examples: ["Ada"] }), footer("Répondre STOP pour vous désinscrire"), buttons([ urlButton({ text: "Suivre", url: "https://genuka.com/t/{{1}}", example: "https://genuka.com/t/42" }), phoneButton({ text: "Appeler", phoneNumber: "+237699001122" }), quickReply("Merci"), ]), ], }); ``` `defineTemplate` returns the exact JSON `POST /{WABA_ID}/message_templates` accepts — store it, diff it, resubmit it, no translation layer. ### What is checked before the network call Everything Meta expresses as a number or a shape. The full list lives in `limits.TEMPLATE` and `limits.TEMPLATE_DEFINITION`; the ones that bite: | Rule | Failure | | --------------------------------------------------------------------------- | ----------------- | | Name is `^[a-z0-9_]+$`, ≤ 512 chars | `ValidationError` | | One HEADER / BODY / FOOTER / BUTTONS / CAROUSEL / LIMITED\_TIME\_OFFER each | `ValidationError` | | Header text ≤ 60 chars, **1 parameter max** | `ValidationError` | | Body ≤ 1024 chars, footer ≤ 60 chars, **no parameter in a footer** | `ValidationError` | | ≤ 10 buttons, ≤ 2 URL, ≤ 1 PHONE\_NUMBER, ≤ 1 COPY\_CODE, ≤ 1 OTP | `ValidationError` | | Button labels are unique; quick replies are listed consecutively | `ValidationError` | | A URL parameter is the **suffix**, and comes with an example | `ValidationError` | | Positional parameters run `{{1}}…{{n}}`, no gaps, no repeats | `ValidationError` | | Named parameters match `^[a-z0-9_]+$` | `ValidationError` | | **One example per parameter** — Meta rejects a template it cannot render | `ValidationError` | | No positional/named mixing, *across the whole template* | `ValidationError` | | `parameter_format` agrees with the components | `ValidationError` | | OTP buttons only on `AUTHENTICATION` | `ValidationError` | What is **not** checked: anything a human reviewer judges — tone, whether the body reads as marketing, whether the URL domain belongs to the business. Those cannot be precomputed, and guessing would block legitimate templates. `validateTemplate(definition)` runs the same checks on a definition that did not come from the builders — raw JSON from an API caller gets identical treatment. ### Parameter style Positional (`{{1}}`) or named (`{{order_id}}`), never both. `defineTemplate` infers `parameter_format` from the components; `templateParameterStyle(components)` reports it. ### Media headers `mediaHeader("IMAGE", handle)` takes a **`header_handle`** from the Resumable Upload API (`POST /{APP_ID}/uploads` → `POST /{UPLOAD_ID}`), *not* a `media_id`. A `media_id` belongs to a phone number and expires after 30 days; a handle belongs to the app and is baked into the approved template forever. Passing one where the other is expected is the most common creation failure. *** ## 2. Special templates ### Carousel ```ts import { carouselTemplate, carouselCard, urlButton } from "@genuka/whatsapp/templates"; const promo = carouselTemplate({ name: "soldes_juin", language: "fr", body: { text: "Nos meilleures offres" }, cards: [ carouselCard({ header: { format: "IMAGE", handle: "4::aW..." }, body: { text: "Sac en cuir, -30%" }, buttons: [urlButton({ text: "Acheter", url: "https://genuka.com/p/1" })], }), carouselCard({ header: { format: "IMAGE", handle: "4::bX..." }, body: { text: "Ceinture assortie, -20%" }, buttons: [urlButton({ text: "Acheter", url: "https://genuka.com/p/2" })], }), ], }); ``` * ≤ 10 cards, ≥ 1 card. * **At least one button per card** (2026 rule) and at most two. * Card body ≤ 160 characters — not 1024. * Top level is BODY + CAROUSEL only: no header, no footer, no buttons. * **Homogeneity.** Every card must have the same media format and the same ordered list of button types. Meta's API accepts a heterogeneous carousel and the human reviewer rejects it days later with "cards must have the same structure". `validateCarousel` refuses it in microseconds instead: ``` ValidationError: carousel.cards[1]: must match the structure of the first card (expected IMAGE|URL, got IMAGE|QUICK_REPLY) ``` ### Limited-Time Offer ```ts const lto = limitedTimeOfferTemplate({ name: "lto_juin", language: "fr", offer: { text: "Expire dans" }, // ≤ 16 characters body: { text: "Profitez de -20% ce week-end" }, buttons: [copyCodeButton("JUIN20"), urlButton({ text: "Boutique", url: "https://genuka.com" })], }); ``` MARKETING only, no footer, header must be IMAGE or VIDEO, and only COPY\_CODE / URL buttons. `has_expiration` defaults to `true`, which shifts a responsibility to the **send** side: ```ts components: [...otherComponents, limitedTimeOfferParameters(Date.now() + 48 * 3600_000)] ``` Forget it and the countdown never renders — the one thing the component exists for. ### Coupon The code appears **twice**: as a body variable and as the `coupon_code` button parameter. Both halves are enforced. ```ts const coupon = couponTemplate({ name: "coupon_juin", language: "fr", body: { text: "Utilisez le code {{1}} avant dimanche", examples: ["JUIN20"] }, couponCode: "JUIN20", // must be one of the body examples }); // at send time, one argument feeds both places: const components = couponSendComponents({ code: "JUIN20", bodyParameters: ["Ada", "JUIN20"] }); // or check a payload built elsewhere: assertCouponCodeMatches(components); ``` `assertCouponCodeMatches` is the guard for payloads that did not come from `couponSendComponents` — it fails when the button says `JUIN20` and the text says `MAI10`, which is exactly what happens when the two are passed separately. ### Authentication ```ts const otp = authenticationTemplate({ name: "otp_login", language: "fr", otpType: "COPY_CODE", // or ONE_TAP / ZERO_TAP // ONE_TAP and ZERO_TAP additionally need: // packageName, signatureHash, and for ZERO_TAP: zeroTapTermsAccepted: true }); // → BODY { add_security_recommendation: true } // FOOTER { code_expiration_minutes: 10 } // BUTTONS [ OTP ] // message_send_ttl_seconds: 600 ``` The body is Meta's, not yours: passing `text` is refused. TTL defaults to 600 s because an OTP arriving after its code expired is worse than one that never arrives — the user retries and burns a second code. At send time the code also goes in two places; `authenticationSendComponents(code)` writes both. *** ## 3. Managing templates ```ts import { MetaTransport } from "@genuka/whatsapp"; import { TemplateClient } from "@genuka/whatsapp/templates"; const client = new TemplateClient({ transport: new MetaTransport({ accessToken }), wabaId, }); await client.create(template); // validates, then POSTs await client.create(template, { validate: false }); // escape hatch, see below await client.list({ status: ["APPROVED", "PAUSED"] }); await client.get(templateId); await client.update(templateId, { components }); // POST /{TEMPLATE_ID} await client.remove({ name, templateId }); // one language when templateId is given ``` `create` validates first because a rejected submission is not free: it counts against the account's template limits and, repeated, against its quality rating. `{ validate: false }` exists for the one case that matters — our validator is deliberately stricter than Graph's 400s, and a caller whose payload Meta already accepts must not be locked out by us. **Editing an approved template resubmits it.** It returns to `PENDING`, and campaigns using it stop launching until review completes. Name and language are immutable. ### Template library (US-3.4) ```ts const page = await client.library({ topic: "ORDER_MANAGEMENT", industry: "E_COMMERCE", language: "fr" }); await client.createFromLibrary({ name: "suivi_livraison", language: "fr", category: "UTILITY", library_template_name: page.data[0].name, library_template_button_inputs: [{ type: "URL", url: { base_url: "https://genuka.com/track" } }], }); ``` Library templates are **approved instantly** — Meta wrote the copy, we only supply the destinations. For a merchant who needs "your order has shipped" today, that is the difference between sending this afternoon and sending next week. `createFromLibrary` skips structural validation on purpose: we do not own the structure. *** ## 4. Lifecycle (US-3.5) `PAUSED` and `DISABLED` arrive **after** approval, because recipients complained. A campaign that starts on a paused template does not fail fast — it fails once per recipient, burns the quota and leaves a trail of 132015s. ```ts import { assertTemplateSendable } from "@genuka/whatsapp/templates"; assertTemplateSendable(template); // throws WhatsAppError, errorClass "template" isTemplateSendable(template); // non-throwing, for greying out a button ``` It accepts our Prisma row (`status: "approved"`) and Meta's resource (`status: "APPROVED"`) alike, and reuses Meta's own codes — 132015 for paused, 132016 for disabled — so a caller's existing error handling reacts identically whether we caught it or Graph did. ``` WhatsAppError: Template "promo_juin" is paused by Meta after negative recipient feedback and cannot be sent. Wait for the pause to lift or edit the template ``` Call it once before the first send of a campaign, not once per recipient. *** ## 5. Analytics (US-3.6) — read this part Two traps, both silent: 1. **Analytics are off by default, per WABA.** Until `client.enableAnalytics()` is called, Meta answers with an empty data set and *no error*. You find out weeks later that nothing was ever collected — and nothing is backfilled. 2. **`read` and `clicked` are purged after 7 days.** Since December 2025 Meta keeps nothing at all beyond a year. A click not written down within the week is gone. ```ts await client.enableAnalytics(); // once per WABA, at connection time const points = await client.analytics({ templateIds: [providerId], // ≤ 10 per call start: unixSeconds, end: unixSeconds, // ≤ 90 days apart }); const day = toAnalyticsDay(points[0]); // { day: "2026-01-01", sent, delivered, read, clicked, cost, clicksByButton } ``` Granularity is `DAILY` and nothing else. **Our archive is the record; Meta is the feed.** `lib/analytics/templates.ts` writes every fetch into `AnalyticsSnapshot` (`kind: "template"`, `subject: