# WasapFlow Bridge β Partner Getting Started Guide **Version:** 2.9.2 **Last Updated:** 4 November 2026 > π **New (4 November 2026) β Support Tickets API.** Your system or AI agent can now open, read and reply to support tickets itself (`POST /tickets`, `GET /tickets`, `GET /tickets/{uid}`, `POST /tickets/{uid}/reply`). See [Support](#support) below. > π **New in 2.9.0 β 32 endpoints, 68 in total.** QR codes, ice breakers and `/` commands, blocking, catalog & cart visibility, call settings, number status, WABA audit log, Flows, number lifecycle, calling and groups. See [Endpoints](#endpoints) below. If your integration works around something because "Bridge does not support it", check that list first β it probably does now. Error responses also carry `meta_code` now, so stop matching on message text. **Already integrated? Use [Prompt C](#which-prompt-should-you-use) β it audits your code against these changes.** > β οΈ **Meta pricing change β effective 1 October 2026.** Service messages (non-template replies inside the 24-hour customer service window) and utility templates sent inside that window become billable. **Nothing about how you send changes** β service messages still need no template and no Meta pre-approval. Meta publishes the new rates by 1 September 2026. See [Meta Pricing β What Changes on 1 October 2026](#meta-pricing--what-changes-on-1-october-2026) below, and the [Changelog](?tab=changelog). --- ## Which prompt should you use? **If you are an AI coding assistant reading this document, read this section before anything else.** Three prompts follow. Use **exactly one**. Using the wrong one is the most common failure β a build-from-scratch prompt applied to a working integration will rewrite code that is already correct. **First, determine which situation applies.** Search the codebase for an existing WasapFlow Bridge integration. Look for: a stored `WF_PARTNER_KEY` or `WF_WEBHOOK_SECRET`, calls to `officialapi.wasapflow.com/bridge/v1`, or a webhook handler that verifies an `x-wasapflow-signature` header. | What you found | Use | |---|---| | **Nothing** β no existing integration, and this is a multi-tenant / reseller platform serving many client businesses | **Prompt A β Build SaaS Integration** | | **Nothing** β no existing integration, and this is for one business using its own WhatsApp number | **Prompt B β Build In-House Integration** | | **An existing integration that already runs** | **Prompt C β Update Existing Integration** | > If you found an existing integration, use **Prompt C**. Do not use A or B. Prompt C audits what is already there against the changelog and changes only what is missing. ---
--- ## Welcome As a WasapFlow Bridge Partner, you get access to WhatsApp Cloud API infrastructure without needing to apply as a Meta Tech Provider yourself. This guide covers everything you need to go from zero to sending your first message in production. --- ## What You Get | Feature | Description | |---------|-------------| | **Partner Key** | Your API authentication key (`x-partner-key` header) | | **Webhook Secret** | For verifying incoming webhooks from WasapFlow | | **Bridge API** | 68 REST endpoints across 24 Meta Cloud API surfaces | | **Number tooling** | QR codes, ice breakers & commands, blocking, commerce settings, call settings | | **Account visibility** | Phone status (OBA, 2FA, name review), WABA audit log, assigned users | | **WABA Management** | Register and manage unlimited client WhatsApp accounts | | **Real-time Logs** | Request logs with status, duration, and error details | | **Billing Dashboard** | Slot invoices β $10 USD per month for every 3 WABAs | | **No SDK needed** | Plain REST β use the HTTP client your stack already has | --- ## Step 1 β Get Your Credentials Login to your partner portal at **https://partner.wasapflow.com** Open **Integration** in the sidebar β you will see: ``` Partner Key : wf_xxxxxxxxxxxxxxxxxxxx [click eye icon to reveal] Webhook Secret : whs_xxxxxxxxxxxxxxxxxxxx [click eye icon to reveal] API Base URL : https://officialapi.wasapflow.com/bridge/v1 ``` > β οΈ **Keep your Partner Key secret.** Anyone with this key can send messages on behalf of your WABAs. If compromised, contact support immediately so we can rotate it. --- ## Step 2 β There Is Nothing to Install **WasapFlow does not ship an SDK.** That is deliberate, not an omission. Bridge is plain REST: two headers, JSON in, JSON out. A client library would be one more dependency to version, audit and wait on β and it would lag every time we add endpoints, which is exactly what happened before we removed it. Use the HTTP client your project already has: `fetch`, `axios`, Guzzle, `requests`, `httpx`, `curl`. **The two headers:** | Header | Value | When | |---|---|---| | `x-partner-key` | `wf_your_key` | Every request | | `x-waba-id` | The client's WABA id | Every request that acts on a specific client's number | **Write this once and you are done:** ```javascript const BASE = 'https://officialapi.wasapflow.com/bridge/v1'; async function bridge(path, { method = 'GET', wabaId, body } = {}) { const res = await fetch(BASE + path, { method, headers: { 'x-partner-key': process.env.WF_PARTNER_KEY, ...(wabaId ? { 'x-waba-id': wabaId } : {}), 'Content-Type': 'application/json', }, body: body ? JSON.stringify(body) : undefined, }); const data = await res.json(); if (!data.success) { const e = new Error(`${data.error.code}: ${data.error.message}`); e.code = data.error.code; e.metaCode = data.error.meta_code; // branch on this, never on message text e.status = res.status; throw e; } return data; } ``` All 68 endpoints work through that one function. Python, PHP and cURL versions are in the [API Reference](?tab=api#calling-bridge-from-your-language). > **All fields are `snake_case`** β requests and responses alike: `waba_id`, > `phone_number_id`, `access_token`. There is no camelCase form of this API. --- ## Step 3 β Get WABA Credentials from Your Client Before registering a client WABA, your client needs to authorise their WhatsApp Business Account. There are two ways to collect credentials: ### Option A: Meta Embedded Signup (Recommended) Implement Meta's Embedded Signup flow in your SaaS frontend. Your clients click "Connect WhatsApp" on your platform, connect their WABA through a Meta popup, and the credentials are registered securely through WasapFlow. **Your app never sees or handles the Meta access token.** #### What is Coexistence mode? WasapFlow Bridge uses **Coexistence mode** for Embedded Signup. Coexistence mode is for clients who already use the **WhatsApp Business App** and want to keep using it after connecting to your SaaS. The same phone number stays active in the WhatsApp Business App, while WasapFlow Bridge also gets Cloud API access for automation, templates, webhooks, and API sending. Use this mode when your client says: - "I already use this number in WhatsApp Business App." - "I still want to reply customers manually from my phone." - "I want my SaaS/CRM/chatbot to connect to the same number." Do not use this mode for: - WhatsApp personal app numbers. - A number that is already fully migrated to Cloud API only. - A manual setup where the client gives you WABA ID, Phone Number ID, and access token. There are two pieces to set: | Where | What to set | Why | |-------|-------------|-----| | Meta popup `FB.login` | `featureType: 'whatsapp_business_app_onboarding'` | Opens Meta's WhatsApp Business App onboarding flow | | WasapFlow register API | `connection_mode: 'coexistence'` | Saves the client WABA as a Coexistence connection | If you use the **WasapFlow Hosted Popup**, both pieces are handled by WasapFlow. If you self-host the Facebook SDK popup, you must pass the `extras` shown below. ``` Your frontend β FB.login β Meta popup β returns CODE β Your backend β POST /clients/register-from-code { code } β WasapFlow β WasapFlow exchanges CODE for TOKEN using platform App Secret (server-side) β WABA registered, token stored encrypted ``` > **Recommended approach: WasapFlow Hosted Popup** β Skip FB.init entirely. WasapFlow hosts the Embedded Signup on `officialapi.wasapflow.com`. Your client sees a popup, connects their WhatsApp, and your frontend receives a postMessage. Meta only sees WasapFlow β never your domain. No domain whitelist required. #### Step 1: Get Embedded Signup config from API Your backend must call the Bridge API to get the Meta App ID and Config ID at runtime. **These values are never hardcoded or exposed in your dashboard** β they are fetched securely via API. ```javascript // Your backend calls this (server-side, not from frontend) const config = await bridge.clients.getEmbeddedSignupConfig(); // Returns: { success: true, app_id: "645...", config_id: "877...", connection_mode: "coexistence", extras: {...} } ``` Or raw HTTP: ```bash curl -H "x-partner-key: wf_your_key" \ https://officialapi.wasapflow.com/bridge/v1/embedded-signup/config ``` > **Do not use your own Meta App.** You must use WasapFlow's App ID so webhooks and token exchange route through our platform correctly. Do not hardcode these values β always fetch from the API so updates are automatic. The returned `extras` are the Coexistence launch settings: ```javascript { setup: {}, featureType: 'whatsapp_business_app_onboarding', sessionInfoVersion: '3', version: 'v4' } ``` #### Step 2: Load the Facebook JS SDK ```html ``` #### Option A: WasapFlow Hosted Popup (Recommended) No FB SDK needed on your side. Just open our hosted page as a popup: This is the easiest way to support Coexistence because WasapFlow automatically launches Meta with `featureType: 'whatsapp_business_app_onboarding'` and registers the result with `connection_mode: 'coexistence'`. **Open the popup from your frontend:** ```javascript document.getElementById('connectBtn').onclick = async function() { const res = await fetch('/api/whatsapp-connect-url'); // your backend const { url } = await res.json(); const popup = window.open(url, 'wasapflow_connect', 'width=480,height=600,left=200,top=100'); window.addEventListener('message', function handler(event) { if (event.data?.type === 'WASAPFLOW_CONNECT_SUCCESS') { window.removeEventListener('message', handler); console.log('WABA connected:', event.data.waba_id); popup.close(); } }); }; ``` **Your backend β generate the connect URL:** ```javascript // Node.js app.get('/api/whatsapp-connect-url', (req, res) => { const url = `https://officialapi.wasapflow.com/bridge/connect` + `?partner_key=${process.env.WF_PARTNER_KEY}` + `&display_name=${encodeURIComponent(req.query.clientName || '')}`; res.json({ url }); }); ``` > **Why this approach:** > - No FB SDK setup needed on your domain > - No domain whitelist required β Meta only sees `officialapi.wasapflow.com` > - Your client never leaves your platform (just a popup) > - Token exchange is 100% server-side on WasapFlow --- #### Option B: Self-hosted (Advanced) Only use this if you need full control. Requires your domain to be whitelisted in WasapFlow's Meta App β contact support to add your domain first. In self-hosted mode, your frontend must pass the Coexistence `extras` to `FB.login`, and your backend must register the returned code with `connectionMode: 'coexistence'`. #### Step 3: Initialize and launch the signup Your frontend receives config from your backend (Step 1), then uses it: ```javascript // Your backend endpoint serves the config to your frontend // e.g. GET /api/whatsapp-config β returns { app_id, config_id, extras } async function initWhatsAppSignup() { // Fetch config from YOUR backend (which calls Bridge API) const resp = await fetch('/api/whatsapp-config'); const { app_id, config_id, extras } = await resp.json(); // Initialize Facebook SDK FB.init({ appId: app_id, version: 'v24.0' }); // When your client clicks "Connect WhatsApp" document.getElementById('connectBtn').onclick = function() { FB.login(function(response) { if (response.authResponse?.code) { // Send code to YOUR backend (not to Meta directly) sendCodeToBackend(response.authResponse.code); } else { alert('WhatsApp connection was cancelled. Please try again.'); } }, { config_id: config_id, response_type: 'code', override_default_response_type: true, extras: { setup: {}, featureType: 'whatsapp_business_app_onboarding', sessionInfoVersion: '3', version: 'v4', ...(extras || {}) } }); }; } initWhatsAppSignup(); ``` #### Step 4: Exchange the code on your backend ```javascript // YOUR backend receives the code from YOUR frontend app.post('/api/connect-whatsapp', async (req, res) => { try { const result = await bridge.clients.registerFromCode({ code: req.body.code, displayName: req.body.clientName || '', connectionMode: 'coexistence' }); if (result.success) { // WABA registered β save result.client.waba_id to your database console.log('Connected:', result.client.waba_id, result.client.phone_number_id); res.json({ success: true, waba: result.client }); } else { res.json({ success: false, error: result.error }); } } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); ``` #### Error handling | Error Code | What Happened | What To Do | |------------|--------------|------------| | `CODE_EXCHANGE_FAILED` | Code expired or invalid | Ask client to redo the Embedded Signup | | `DISCOVERY_FAILED` | Client didn't complete the signup flow | Ensure client selects a WABA and phone number in the popup | | `WABA_LIMIT_REACHED` | Partner hit max WABAs | Upgrade plan or remove unused WABAs | | `PLATFORM_NOT_CONFIGURED` | WasapFlow admin hasn't set Meta credentials | Contact WasapFlow support | #### Important notes - **Popup blocking:** Some browsers block popups. If `FB.login` doesn't trigger, ensure it's called from a direct user click event (not from an async callback). - **Mobile browsers:** The Meta popup may not work well on some mobile browsers. Consider showing a "Please use desktop" message, or provide the manual registration option (Option B) as fallback. - **Duplicate signup:** `register-from-code` performs an upsert β safe to call again if the same client reconnects. Refer to Meta docs: [Embedded Signup](https://developers.facebook.com/docs/whatsapp/embedded-signup) ### Option B: Manual Registration (from Meta Business Manager) For clients who can't use Embedded Signup (e.g., mobile users), they can provide credentials manually: 1. Go to **business.facebook.com** β **WhatsApp Accounts** 2. Find: **WABA ID**, **Phone Number ID**, and generate a **permanent access token** via System Users 3. Register via API: ```javascript await bridge.clients.register({ wabaId: '123456789', phoneNumberId: '987654321', accessToken: 'EAAxxxxxxxx', displayName: 'My Client Business' }); ``` > **Security note:** With this method, your app handles the Meta access token directly. Use `register-from-code` (Option A) whenever possible β it's more secure because the token is never exposed to your app. --- ## Step 4 β Register a Client WABA Once you have credentials, register the WABA with WasapFlow. This is done **once per client**. ### Using the API directly: ```bash curl -X POST https://officialapi.wasapflow.com/bridge/v1/clients/register \ -H "x-partner-key: wf_your_partner_key" \ -H "Content-Type: application/json" \ -d '{ "waba_id": "123456789", "phone_number_id": "987654321", "access_token": "EAAxxxxxxxx", "display_name": "My Client Business" }' ``` ### From JavaScript ```javascript const data = await bridge('/clients/register', { method: 'POST', body: { waba_id: '123456789', phone_number_id: '987654321', access_token: 'EAAxxxxxxxx', display_name: 'My Client Business', }, }); console.log(data.client.waba_id); ``` ### From Python ```python import os, requests res = requests.post( 'https://officialapi.wasapflow.com/bridge/v1/clients/register', headers={'x-partner-key': os.environ['WF_PARTNER_KEY']}, json={ 'waba_id': '123456789', 'phone_number_id': '987654321', 'access_token': 'EAAxxxxxxxx', 'display_name': 'My Client Business', }, timeout=15, ) data = res.json() if not data.get('success'): raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}") ``` ### From PHP ```php $res = $http->post('clients/register', [ 'headers' => ['x-partner-key' => getenv('WF_PARTNER_KEY')], 'json' => [ 'waba_id' => '123456789', 'phone_number_id' => '987654321', 'access_token' => 'EAAxxxxxxxx', 'display_name' => 'My Client Business', ], 'http_errors' => false, ]); $data = json_decode((string) $res->getBody(), true); ``` **Successful response:** ```json { "success": true, "client": { "id": 42, "waba_id": "123456789", "phone_number_id": "987654321", "display_name": "My Client Business", "quality_rating": "GREEN", "tier": "TIER_1K", "messaging_limit_tier": "TIER_1K", "status": "active", "registered_at": "2026-05-12T10:00:00Z" } } ``` --- ## Step 5 β Send Messages Once a WABA is registered, you can send messages immediately. ### Send a text message ```javascript const waba = bridge.client('123456789'); await waba.messages.send({ to: '60123456789', // phone number with country code, no + text: 'Hello from WasapFlow!' }); ``` ### Send a template message ```javascript await waba.messages.template({ to: '60123456789', template: 'order_confirmed', language: 'en_US', params: ['John Doe', 'RM150.00', 'ORD-001'] }); ``` > β οΈ **Templates must be pre-approved by Meta** before you can use them. See [Step 7 β Managing Templates](#step-7--managing-templates). ### Send media (image / document / audio / video) ```javascript await waba.messages.media({ to: '60123456789', type: 'image', url: 'https://yourserver.com/image.jpg', caption: 'Your order receipt' }); ``` ### Send interactive buttons ```javascript await waba.messages.buttons({ to: '60123456789', body: 'Choose an option:', buttons: [ { id: 'yes', title: 'Confirm' }, { id: 'no', title: 'Cancel' } ] }); ``` ### Send interactive list ```javascript await waba.messages.interactive({ to: '60123456789', type: 'list', header: 'Choose a service', body: 'Please select from the list below', footer: 'WasapFlow', button: 'View Options', sections: [{ title: 'Services', rows: [ { id: 'svc_1', title: 'Support', description: 'Talk to support' }, { id: 'svc_2', title: 'Sales', description: 'Talk to sales' } ] }] }); ``` --- ## Step 6 β Receive Webhooks WasapFlow forwards Meta webhook events to your `Webhook URL` (set in the partner portal under Integration β Webhook). ### Set your webhook URL Go to **Settings** in your partner portal β enter your server's public HTTPS URL. ``` https://yourapp.com/webhooks/wasapflow ``` > **Requirements:** Must be HTTPS, publicly accessible, and respond with HTTP 200 within **10 seconds**. ### Verify incoming webhooks Every webhook is signed with HMAC-SHA256 using your **Webhook Secret**. Always verify before processing. ```javascript const crypto = require('crypto'); app.post('/webhooks/wasapflow', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-wasapflow-signature']; const expected = 'sha256=' + crypto .createHmac('sha256', process.env.WF_WEBHOOK_SECRET) .update(req.body) .digest('hex'); if (signature !== expected) { return res.status(401).send('Unauthorized'); } // Always respond 200 FIRST, then process (avoid timeout) res.status(200).send('OK'); const { event, waba_id, data } = JSON.parse(req.body); switch (event) { case 'message.received': // π data.bsuid = Business-Scoped User ID (stable across WhatsApp username changes, Jun 2026+) // Recommended: store BOTH data.from (phone) AND data.bsuid in your contact record console.log(`New message from ${data.from} (bsuid: ${data.bsuid}): ${data.text?.body}`); // β save to DB, trigger reply, etc. break; case 'message.delivered': console.log(`Delivered to ${data.to} β msg id: ${data.message_id}`); break; case 'message.read': console.log(`Read by ${data.to}`); break; case 'message.failed': console.log(`Failed: [${data.error_code}] ${data.error_message}`); break; // π Coexistence β reply typed manually in the WhatsApp Business App case 'message.echo': // data.direction = "outbound", data.source = "business_app" console.log(`Business-App reply to ${data.recipient}: ${data.text}`); // β store as OUTBOUND message (de-dupe by data.message_id) break; // π Coexistence β past conversations backfilled once after onboarding case 'message.history': // data.history = true; use data.timestamp (original time) for ordering console.log(`History (${data.progress}%) ${data.direction}: ${data.text}`); // β store as backfill; do NOT trigger bots/auto-replies break; // π Coexistence β contact added/edited in the Business App case 'contact.synced': console.log(`Contact ${data.action}: ${data.full_name} (${data.phone_number})`); break; // π Template lifecycle case 'template.status_updated': console.log(`Template ${data.template_name} β ${data.status}` + (data.reason ? ` (${data.reason})` : '')); // β only send templates whose status is APPROVED break; case 'template.quality_updated': console.log(`Template ${data.template_name} quality ${data.previous_quality} β ${data.new_quality}`); break; case 'template.category_updated': console.log(`Template ${data.template_name} category β ${data.new_category}`); break; case 'waba.quality_updated': console.log(`WABA ${waba_id} quality β ${data.quality_rating}`); break; case 'waba.tier_updated': console.log(`WABA ${waba_id} tier β ${data.tier}`); break; // π Account standing case 'waba.account_updated': console.log(`WABA ${waba_id} account event β ${data.event}`); break; case 'waba.review_updated': console.log(`WABA ${waba_id} review β ${data.decision}`); break; } }); ``` ### Webhook Retry Policy If your endpoint is **unreachable or returns non-200**, WasapFlow will retry automatically: | Attempt | Delay | |---------|-------| | 1st retry | 2 seconds | | 2nd retry | 4 seconds | | 3rd retry | 8 seconds | | After 3 fails | Event dropped, logged in your dashboard | > **Tip:** Always respond `200 OK` immediately and process asynchronously. Your endpoint must respond within **10 seconds** or the attempt counts as failed. ### Webhook Events Reference | Event | When | |-------|------| | `message.received` | Client received an inbound WhatsApp message | | `message.sent` | Outbound message accepted by Meta | | `message.delivered` | Message delivered to recipient's phone | | `message.read` | Recipient opened the message | | `message.failed` | Message failed β check `data.error_code` and `data.error_message` | | `message.echo` π | Client sent a message **manually from the WhatsApp Business App** (Coexistence) β store as outbound | | `message.history` π | Past conversation replayed once after onboarding (Coexistence backfill, `data.history = true`) | | `contact.synced` π | Contact added/edited/removed in the Business App (Coexistence) β `data.action` | | `template.status_updated` π | Template approved/rejected by Meta β `data.status` (send only when `APPROVED`) | | `template.quality_updated` π | Template quality score changed (GREEN / YELLOW / RED) | | `template.category_updated` π | Meta re-categorised a template (may affect pricing) | | `waba.quality_updated` | WABA quality rating changed (GREEN / YELLOW / RED) | | `waba.tier_updated` | WABA messaging tier changed (affects daily limit) | | `waba.account_updated` π | WABA account status changed (verified / disabled / restricted) | | `waba.review_updated` π | Business verification review decision β `data.decision` | | `waba.alert` | Meta sent a business alert for the WABA | --- ## Step 7 β Managing Templates WhatsApp templates must be **pre-approved by Meta** before use. You manage templates via Meta Business Manager for each client WABA. ### How to create a template for a client: 1. Go to **business.facebook.com** β select your client's Business account 2. Navigate to **WhatsApp Manager** β **Message Templates** 3. Click **Create Template** 4. Choose category: `MARKETING`, `UTILITY`, or `AUTHENTICATION` 5. Fill in template name, language, and body (with `{{1}}`, `{{2}}` placeholders) 6. Submit for review β Meta usually approves within **a few minutes to 24 hours** ### Template status codes: | Status | Meaning | |--------|---------| | `APPROVED` | Ready to use | | `PENDING` | Under Meta review | | `REJECTED` | Failed Meta review β check Meta's rejection reason | | `PAUSED` | Too many user opt-outs β improve message quality | | `DISABLED` | Meta disabled β contact Meta support | > **Error `#132001`** = Template name doesn't exist or language mismatch. Double-check the exact template name and `language_code` (e.g., `en_US`, `ms`). --- ## Step 8 β Managing Access Tokens Meta access tokens can **expire**. A permanent token via System User is strongly recommended. ### Create a permanent System User Token: 1. Go to **business.facebook.com** β **Business Settings** β **System Users** 2. Create a system user β assign **Admin** role to the WABA 3. Generate a token β select `whatsapp_business_management` and `whatsapp_business_messaging` permissions 4. This token does **not expire** unless manually revoked ### Update a WABA's access token in WasapFlow: Call `register` again with the new token β it does an upsert (updates if already exists): ```bash curl -X POST https://officialapi.wasapflow.com/bridge/v1/clients/register \ -H "x-partner-key: wf_your_partner_key" \ -H "Content-Type: application/json" \ -d '{ "waba_id": "123456789", "phone_number_id": "987654321", "access_token": "EAAnew_token_here" }' ``` Or use the `refresh` endpoint to update token + sync quality rating in one call: ```bash curl -X POST https://officialapi.wasapflow.com/bridge/v1/clients/123456789/refresh \ -H "x-partner-key: wf_your_partner_key" \ -H "Content-Type: application/json" \ -d '{ "access_token": "EAAnew_token_here" }' ``` ### Reconnect Meta Webhook If a client's WABA stops receiving webhook events (messages not arriving, delivery receipts missing), you can reconnect the webhook without re-registering the WABA: ```bash curl -X POST https://officialapi.wasapflow.com/bridge/v1/clients/123456789/resubscribe-webhook \ -H "x-partner-key: wf_your_partner_key" ``` This re-subscribes the WABA to WasapFlow's Meta webhook endpoint. Safe to call at any time β idempotent. **When to use this:** - Webhook events suddenly stopped arriving for a specific WABA - After token refresh if webhooks remain broken - After any Meta App settings change by the platform --- ## Step 9 β Testing Before Going Live Use Meta's test phone numbers to test without real WhatsApp accounts. ### Using Meta Test Numbers: 1. In **Meta for Developers** β your app β **WhatsApp** β **API Setup** 2. Use the **Test phone number** provided (no real account needed) 3. Add a recipient number in **"To"** β verify via OTP ### Verify a phone number is on WhatsApp: ```javascript const check = await bridge.contacts.check('60123456789'); console.log(check.exists); // true / false ``` > **Best practice:** Always check if a number is on WhatsApp before sending to avoid failed deliveries and Meta penalties. --- ## Meta Pricing β What Changes on 1 October 2026 Read this even if you do not handle billing yourself. It changes your cost base. ### The short version Meta charges per delivered message, by category and by the recipient's country. Two categories that are free today start costing money on **1 October 2026**: | What | Free since | Charged from | |---|---|---| | **Service messages** β any non-template reply inside the 24-hour customer service window | 1 Nov 2024 | 1 Oct 2026 | | **Utility templates** delivered inside an open customer service window | 1 Jul 2025 | 1 Oct 2026 | ### What does NOT change This is a billing change, not a policy change. Specifically: - Service messages remain **free-form**. No template. **No Meta pre-approval.** - The 24-hour customer service window works exactly as before. - You still need an approved template only to *start* a conversation or to reply after the window has closed. - The **72-hour free entry point window** (Click to WhatsApp Ads, Page CTA button) stays free. If you were worried you would have to convert your automated replies into approved templates β you do not. ### What to know about the new charges - Service messages are charged at the **same rate as utility and authentication** messages for that market. - **There are no volume tiers for service messages.** Utility and authentication keep their tiers; service does not. Higher volume does not lower the rate. - **One charge per message.** A non-template message containing promotional content does *not* additionally incur a marketing charge. - Meta publishes the rates effective 1 October **by 1 September 2026**. ### Work out your exposure now Every message status webhook carries a `pricing` object. The messages that become billable are exactly those where `pricing.type` is `free_customer_service`: ```javascript // In your webhook handler if (event.startsWith('message.') && data.pricing) { await db.messagePricing.upsert({ messageId: data.message_id, billable: data.pricing.billable, type: data.pricing.type, // 'regular' | 'free_customer_service' | 'free_entry_point' category: data.pricing.category, // 'service' | 'utility' | 'marketing' | ... origin: data.conversation?.origin ?? null }); } ``` ```sql -- Your added monthly cost = this count Γ the rate Meta publishes SELECT category, COUNT(*) AS becomes_billable FROM message_pricing WHERE type = 'free_customer_service' AND created_at >= NOW() - INTERVAL '30 days' GROUP BY category; ``` `pricing` is absent on some events (commonly `read`). Always null-check it. > **Do not hardcode rates.** They vary by country and Meta may change them quarterly. Count volume now; apply the rate when it is published. ### Meta Business Agent β not you, but worth knowing On 1 July 2026 Meta launched its own AI agent as a separate message category, billed per token from 1 August 2026. Meta's documentation is explicit that any non-template message *not* powered by Meta Business Agent β including messages from a human agent **or a third-party AI solution** β is a **service** message. Everything you send through Bridge is service traffic. It matters to you for one operational reason, covered next. ### Build this: the "Meta Business Agent took over" banner If a client enables Meta Business Agent on their number, Meta's AI and your app coexist on that number and only one is the **active handler** at a time. While Meta's agent holds the conversation, Meta **stops sending you `message.received`** and sends `standby.*` events instead. From your client's point of view, their automation simply stops replying. Nothing errors. **Nothing appears in your logs** β the messages never reached you. They will open a ticket saying the bot is dead, and you will have nothing to show them. **Please build a persistent warning banner** β in your app header and in the inbox for the affected number: - **Raise it** on any `standby.*` event - **Clear it** when normal `message.received` events resume - Use a **warning** style (yellow/amber), not an error style β nothing has broken - Make the raise **idempotent**; standby events can arrive many times a minute Suggested wording: > π€ **Meta Business Agent is answering your customers β not your AI.** To take back control, turn it off in WhatsApp Manager β Account tools β Business Agent. Manual chat still works normally. And critically β **never auto-reply while in standby**: ```javascript if (data.standby) { await alerts.raise({ clientId, type: 'meta_agent_active', severity: 'warning' }); return saveContextOnly(data); // observe only } ``` Sending a service message during standby makes *your* app the active handler, which is the correct way to escalate to a human β but if a bot does it automatically, you end up with two AIs fighting over one conversation. WasapFlow ships this exact banner in its own product. Mirroring it means your clients get an explanation instead of a mystery. Full payload reference: [`standby.*` events](?tab=api#standby--meta-business-agent-is-handling-the-conversation-). ### Pricing calendar Meta may change rates only on 1 January, 1 April, 1 July, or 1 October, with at least one month's notice for a rate card update. You will see any pending change in the `X-Bridge-Notice` header on every API response before it takes effect. --- ## Monitoring β What to Watch ### Dashboard (partner.wasapflow.com/dashboard) - **Active WABAs** β how many client accounts are registered - **Requests (24h)** β total API calls in last 24 hours - **Success / Errors** β breakdown of request outcomes ### Logs (partner.wasapflow.com/logs) Each log row shows: - **Time** β when the request was made - **WABA ID** β which client account - **Direction** β `outbound` (you β Meta) or `inbound` (Meta β you) - **Endpoint** β API endpoint called - **Status** β HTTP response code (200 = ok, 4xx/5xx = error) - **Meta Error Code** β if Meta returned an error (e.g. `#132001` = template not found) - **Duration** β response time in ms ### WABAs (partner.wasapflow.com/wabas) - List of all registered client WABAs - Status of each (active/inactive) - Last activity timestamp - Quality rating and throughput tier (from Meta) ### WABA Quality & Tier | Quality | Meaning | Action | |---------|---------|--------| | π’ GREEN | Good standing | No action needed | | π‘ YELLOW | Warning β user complaints increasing | Review message content | | π΄ RED | High complaints β messaging limit may drop | Pause campaigns, review opt-ins | | Tier | Daily Message Limit | |------|---------------------| | TIER_1K | 1,000 conversations/day | | TIER_10K | 10,000 conversations/day | | TIER_100K | 100,000 conversations/day | | TIER_UNLIMITED | Unlimited | --- ## Billing β WABA Slots ### How it works - **Creating an account is free** β no credit card, no expiry - **Your first number is free for 3 days, no card needed.** One free window per account, for the first number only - Add a card before the 3 days end to keep it: the first $10.00 is charged when the free days end and your monthly cycle starts from that date - No card by then β the number **pauses** (not deleted; webhooks and sends stop) and switches back on as soon as you add a card - A second number needs a card (`402 CARD_REQUIRED`). If you have already used your free window, your first WABA opens a slot and $10.00 is charged that day - One slot holds **3 active WABAs**. The 4th WABA opens a second slot, prorated for the rest of the cycle - Removing WABAs lowers your slot count from the next cycle; days already paid are not refunded ### Pricing | Active WABAs | Slots | Per month | |------|------|------| | 0 | 0 | **Free** | | 1 β 3 | 1 | **$10.00 USD** (net) | | 4 β 6 | 2 | $20.00 USD | | 7 β 9 | 3 | $30.00 USD | | + Stripe processing fee | | $0.31 flat, per invoice | ### Managing billing - **Save card** β click "Save Card" in the Billing page. Nothing is charged before your free days end (or, without a free window, before your first WABA) - **Update card / download invoices** β click "Manage Billing" β Stripe Customer Portal - **Payment failed** β API is blocked immediately. Click "Pay Now" link in dashboard to pay outstanding invoice and restore access --- ## API Reference Summary **Base URL:** `https://officialapi.wasapflow.com/bridge/v1` **Auth header:** `x-partner-key: wf_your_key` **Content-Type:** `application/json` ### Standard Response Format ```json // Success { "success": true, "data": { ... } } // Error { "success": false, "error": { "code": "ERROR_CODE", "message": "Human readable message" } } ``` ### Endpoints 68 endpoints across 24 Meta Cloud API surfaces. Full request and response detail for each is in the [API Reference](?tab=api) β this is the index. **Clients (WABAs)** | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/clients/register` | Register a client WABA (upsert) | | `POST` | `/clients/register-from-code` | Register from an Embedded Signup code | | `GET` | `/clients` | List all registered WABAs | | `DELETE` | `/clients/:wabaId` | Remove a WABA | | `POST` | `/clients/:wabaId/refresh` | Refresh quality/tier + optional token update | | `POST` | `/clients/:wabaId/resubscribe-webhook` | Reconnect Meta webhook for a WABA | | `POST` | `/connect/session` | Create a 30-minute onboarding link (**use this, never `?partner_key=`**) | | `GET` | `/embedded-signup/config` | Meta App ID + config ID for the JS SDK | | `GET` | `/onboarding/events` | Onboarding session log β why a signup failed | **Messaging** | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/messages/send` | Send text | | β | *(any send endpoint)* | Add `context: { message_id }` for a quoted reply β see [API reference](?tab=api#quoted-replies-context) | | `POST` | `/messages/template` | Send template | | `POST` | `/messages/media` | Send image / document / audio / video | | `POST` | `/messages/interactive` | Buttons, lists, catalog & product messages | | `POST` | `/messages/location` | Send location | | `POST` | `/messages/reaction` | React to a message | | `POST` | `/messages/read` | Mark as read | | `POST` | `/media/upload` | Upload media to Meta, get `media_id` | | `GET` | `/media/:mediaId` | Download media | | `GET` | `/contacts/:phone` | Check if a phone is on WhatsApp | **Templates & broadcasts** | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/templates` | List templates | | `POST` | `/templates` | Create a template | | `POST` | `/templates/upload-header` | Upload a media header | | `DELETE` | `/templates/:templateName` | Delete a template | | `POST` | `/broadcasts` | Start a broadcast | | `GET` | `/broadcasts` Β· `/broadcasts/:id` | List / inspect broadcasts | | `POST` | `/broadcasts/:id/cancel` | Cancel a running broadcast | **Number setup β new in 2.9.0** | Method | Endpoint | Description | |--------|----------|-------------| | `GET` `POST` | `/qr-codes` | List / create QR codes & short links | | `PUT` `DELETE` | `/qr-codes/:code` | Change the message (image stays valid) / delete | | `GET` `POST` | `/conversational-automation` | Ice breakers, `/` commands, welcome message | | `GET` `POST` `DELETE` | `/blocked` | List / block / unblock users | | `GET` `POST` | `/commerce-settings` | Show or hide catalog and cart | | `GET` `POST` | `/settings` | Call settings and No-Storage mode | | `GET` | `/phone-status` | OBA status, 2FA state, display-name review | **Account & diagnostics β new in 2.9.0** | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/waba` | Account details + `health_status` (why sends are blocked) | | `GET` | `/waba/activities` | Audit log β who changed what, and when | | `GET` | `/waba/assigned-users` | Which BM users can access this WABA | | `GET` | `/waba/solutions` | Solutions attached to this WABA | | `GET` | `/schedules` | Pending scheduled changes | | `GET` | `/flows` Β· `/flows/:flowId` | List / inspect Flows (read-only) | | `GET` | `/usage` | Your own usage and billing counters | | `GET` | `/analytics` | Message analytics | | `GET` | `/health` | Service health (no auth) | **Number lifecycle β new in 2.9.0, changes the number itself** | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/phone/request-code` | Request an SMS/voice verification code | | `POST` | `/phone/verify-code` | Submit the code | | `POST` | `/phone/two-step` | Set the 2FA PIN (**see the warning below**) | | `POST` | `/phone/register` | Re-register the number | | `POST` | `/phone/deregister` | Deregister β needs `{"confirm": true}` | > β οΈ **If you have ever changed a two-step PIN outside Bridge**, set it once via > `POST /phone/two-step`. Bridge needs that PIN to re-register a number during a > migration or recovery. Before 2.9.0 we only held one platform PIN, so a PIN > changed in WhatsApp Manager broke our re-registration **silently** β and you > only found out during a recovery, when it mattered most. **Calling & groups β new in 2.9.0** | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/calls` | `connect`, `pre_accept`, `accept`, `reject`, `terminate` | | `GET` `POST` | `/groups` | List / create groups | | `GET` | `/groups/:groupId` | Group info | | `DELETE` | `/groups/:groupId` | Delete a group β needs `{"confirm": true}` | | `DELETE` | `/groups/:groupId/participants` | Remove participants (max 8) | > Calling needs `calling.status: "ENABLED"` via `POST /settings` first, and the > customer's permission for business-initiated calls. > > **Groups must be enabled on your number by Meta.** If it is not, Meta answers > `131000 "Something went wrong"` β a message that explains nothing. We confirmed > this against a live WABA. That is not a Bridge fault; Bridge passes Meta's status > through unchanged. **Webhook recovery** | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/webhooks/failed` | Events we could not deliver to you | | `POST` | `/webhooks/retry/:eventId` Β· `/webhooks/retry-all` | Redeliver | **Support tickets** π | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/tickets` | Open a ticket (deduplicated by subject within 24h; max 20 new per 24h) | | `GET` | `/tickets?status=` | List your tickets | | `GET` | `/tickets/:uid` | Read a ticket and its messages | | `POST` | `/tickets/:uid/reply` | Reply (reopens a closed ticket) | --- ## Rate Limits - Default: **200 requests per second** per partner - If exceeded: `429 Too Many Requests` with `Retry-After` header - Contact support to increase limit if needed **Recommended retry pattern:** ```javascript async function sendWithRetry(fn, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (err) { if (err.status === 429) { const wait = (err.retryAfter || 1) * 1000; await new Promise(r => setTimeout(r, wait)); } else throw err; } } } ``` --- ## Security Best Practices 1. **Never hardcode credentials** β always use environment variables ```bash # .env WF_PARTNER_KEY=wf_xxxxxxxxxxxx WF_WEBHOOK_SECRET=whs_xxxxxxxxxxxx ``` 2. **Always verify webhook signatures** β reject requests without valid `x-wasapflow-signature` 3. **Use HTTPS only** for your webhook endpoint 4. **Store access tokens encrypted** β never store Meta tokens in plain text in your DB 5. **Rotate your Partner Key periodically** β regenerate from dashboard every 3β6 months 6. **Restrict API access by IP** β if your server has a fixed IP, whitelist it in your firewall --- ## Common Errors | HTTP | Error Code | Meta Error | Meaning | Fix | |------|-----------|-----------|---------|-----| | `400` | `WABA_NOT_FOUND` | β | WABA not registered | Register the WABA first | | `400` | β | `#132001` | Template not found or wrong language | Check template name + language code in Meta BM | | `400` | β | `#131026` | Phone not on WhatsApp | Use `/contacts/:phone` to check first | | `400` | β | `#130429` | Meta rate limit (per WABA) | Slow down sending | | `400` | β | `#131047` | Re-engagement window expired | Can only reply 24h after last customer message for free-form text | | `401` | `INVALID_KEY` | β | Invalid partner key | Check `x-partner-key` header | | `402` | `PAYMENT_REQUIRED` / `CARD_REQUIRED` | β | No card saved (only the first number is free for 3 days), or payment failed | Save a card, or pay the outstanding invoice | | `403` | `SUBSCRIPTION_CANCELLED` | β | Account cancelled | Resubscribe via Billing page | | `429` | `RATE_LIMIT` | β | WasapFlow rate limit hit | Reduce request rate or contact support | | `500` | `META_ERROR` | β | Meta API error | Retry β if persistent, check Meta status page | --- ## FAQ **Q: Can I register multiple phone numbers under one WABA?** A: One WABA registration in WasapFlow corresponds to one phone number ID. If your client has multiple numbers, register each separately. **Q: What does it cost to start?** A: Nothing. A partner account is free and never expires, and your first number is free for 3 days without a card. Add a card before the 3 days end to keep it; $10 is then charged and that slot covers up to 3 WABAs. **Q: Can I remove a WABA and add it back later?** A: Yes. Removing a WABA stops new API access for that WABA immediately, but **if it was active at any point during the billing month, it still counts toward your peak WABA count** for that invoice. Re-registering restores full access. Re-registering the same WABA ID in the same month does not increase peak beyond what was already recorded. **Q: My client's access token expired. What now?** A: Generate a new permanent token from Meta Business Manager (System Users), then call `POST /clients/register` again with the new token (it's an upsert β safe to call on existing WABAs). Alternatively, call `POST /clients/:wabaId/refresh` with `{ "access_token": "new_token" }` to update and sync quality rating in one step. **Q: A client's webhook events stopped arriving (messages not showing, no delivery receipts). What now?** A: Call `POST /clients/:wabaId/resubscribe-webhook` to re-subscribe the WABA to WasapFlow's Meta webhook. This is safe to call at any time and does not affect messaging. If the issue persists, also call `/refresh` to ensure the token is valid. **Q: Can I white-label the dashboard for my clients?** A: The partner dashboard is for your own use to manage your WABAs. Your clients interact with WhatsApp directly through your SaaS app β they don't need access to the WasapFlow partner portal. **Q: Is there a sandbox/test mode?** A: Use Meta's test phone numbers (available in your Meta developer app) for testing without real WhatsApp accounts. **Q: What Meta permissions does my app need?** A: `whatsapp_business_management` and `whatsapp_business_messaging`. Your app doesn't need to be verified if you're using WasapFlow as the Tech Provider. --- ## Support Open a **support ticket** β from the partner portal (**Support** in the sidebar, screenshots allowed) or from your own system / AI agent with `POST /tickets` (see [Support Tickets](?tab=api#support-tickets) in the API Reference). You get an email when we reply. Email **support@wasapflow.com** only if you cannot reach the portal. Do not open tickets for validation errors in your own requests (`MISSING_FIELDS`, `INVALID_PAYLOAD`, a closed 24-hour window) β the error message already tells you what to fix. Include in your ticket: - Your Partner Key (first 8 chars only β **never share full key**) - WABA ID affected - Timestamp of the issue (UTC) - Error message or Meta error code - Request payload (redact any sensitive tokens) **Response time:** Business hours (GMT+8), typically within 4 hours. --- *This document is confidential and intended for WasapFlow Bridge Partners only. Do not distribute.* *Β© 2026 Kobaranteguh Sdn Bhd β All rights reserved.*