WhatsApp Business, on your own infrastructure.
A per-tenant bridge to the Meta WhatsApp Business Cloud API. Your Meta App, your Phone Number, your Access Token —
every message and every media blob lands on your own IIS filesystem. No CodeB relay in the middle, no third-party BSP,
no per-message markup. HMAC-verified webhooks, appsecret_proof on every Graph call, per-message-id dedup,
and a shared “WhatsApp Inbox” group thread so your team replies from the same chat UI they already use.
Included at zero cost with CodeB Sovereign Communications. You pay Meta directly for WhatsApp conversations.
What ships in the box
Everything below is code that lives in /whatsapp.ashx today. Feature list mirrors what the admin UI can inspect.
Inbound webhook (HMAC-verified)
Meta posts events to /whatsapp.ashx?action=webhook. Every POST is HMAC-SHA256-verified against the tenant’s App Secret in constant time. Forged events with a bad signature get [RASP-WA-WEBHOOK] reject=bad-signature in the diag log and a 401 response.
Outbound send (Graph API v20.0)
POST /whatsapp.ashx?action=send with {to, body}. Posts to graph.facebook.com/v20.0/<phone-number-id>/messages with appsecret_proof appended, so calls survive Meta’s “Require app secret” toggle.
Media caching
Images, audio, video, documents and stickers get fetched from Meta’s lookaside CDN and cached under App_Data/<tenant>/chat-whatsapp/media/<mediaId> with a .meta.json sidecar. Served back via ?action=media&id=<id>, admin-gated. Nothing leaves your infrastructure after the initial Meta fetch.
Per-message dedup
Meta retries webhooks on 5xx or timeout. We use a filename-existence check on messages/wa-<msg-id>.json before writing — Meta message IDs are globally unique, so file-exists is a bulletproof seen-set. Retries log [WA-DEDUP] skip-duplicate instead of double-posting.
Shared “WhatsApp Inbox” group
Every inbound message drops into one shared group thread in chat.html. Your team sees WhatsApp conversations alongside internal DMs and channel messages. No context-switching between apps.
Admin UI
/whatsapp-admin.html ships the config CRUD form, Meta Dashboard values panel with copy buttons, verify-token rotate, test-send widget, webhook subscription reference, and a live activity feed with per-event content preview.
Two-tier admin gate
Config edits and outbound send require either an OIDC Bearer with admin role (validated via loopback to /oidc.ashx?action=userinfo) OR the shared-secret HMAC header. Every rejection logs an [WA-ADMIN-GATE] diag with the client IP.
Robust audit log
Every webhook and every outbound send is written to chat-whatsapp/audit/YYYY-MM-inbound.jsonl / -outbound.jsonl as JSON-per-line. The server pre-extracts a summary object at write time (from, contact, textPreview, statusType) so the admin UI can render meaningful previews without walking deep Meta payload structures.
Phase 2 — Bidirectional bridge, templates, abuse guard
The base connector above handled inbound webhooks, outbound send, media caching and the shared Inbox group. Phase 2 turns it into a full two‑way channel: per‑customer group threads in chat.html, Meta‑approved message templates with live placeholder preview, template lifecycle admin, an API‑key‑gated external send endpoint with optional template binding, a per‑day new‑chat quota, and a live 24‑hour customer service window pill so operators never type into a closed window. Every capability below is code in /whatsapp.ashx, /chat.ashx and /whatsapp-admin.html today.
Bidirectional chat.html bridge
Inbound WhatsApp messages create (or join) a per‑remote‑number group gc-wa-<E.164 digits>. Outbound replies typed in chat.html forward to Meta via a cross‑.ashx loopback between /chat.ashx?op=send and /whatsapp.ashx?action=send. Per‑user routing lets admins decide which internal users see WA groups.
Template send with live placeholder preview
The new‑conversation modal in chat.html ships a Meta‑approved template picker. Each {{n}} placeholder gets its own input and the rendered message updates as the operator types. Same flow works for cold outreach and for reopening a customer service window that has expired.
Template lifecycle admin
/whatsapp-admin.html lists APPROVED, PENDING, IN_APPEAL and PAUSED templates from Meta with per‑template Delete. The “Submit new template” form validates placeholder count against example values before Meta’s review; a preset button loads the Brochterbeck‑host hospitality template as a working example.
Template status toasts over WebSocket
Meta’s message_template_status_update webhook events flow through as WebSocket toasts to every routing user’s chat.html. APPROVED renders green and auto‑dismisses; REJECTED, PAUSED and DISABLED render sticky red with Meta’s exact reason string.
API‑key‑gated external send
POST /whatsapp.ashx?action=send-external with header X-CodeB-WhatsApp-API-Key. Keys are 40 chars in the shape wa_live_<32 hex>, minted once (plaintext shown once at generation, then masked forever), constant‑time‑compared, uniform 401 on any auth failure — no oracle on which field was wrong.
Template‑bound API keys
Each key can optionally be pinned to a specific approved template plus a language. A bound key can ONLY send that template — free‑form is refused with 403 template-required, other templates with 403 template-mismatch, wrong language with 403 template-language-mismatch. Blast radius from a leaked key is exactly one Meta‑reviewed template.
External template list endpoint
GET /whatsapp.ashx?action=templates-list-external under the same API‑key auth. A bound key sees only its pinned template; an unrestricted key sees every APPROVED template. Third‑party integrations render their own pickers without holding Meta credentials.
3‑placeholder cap with placeholder metadata
Templates are capped at 3 {{n}} placeholders per body (constant WA_TEMPLATE_MAX_PLACEHOLDERS). Enforced at BOTH creation (HandleCreateTemplate returns 400 too-many-placeholders for >3 and 400 placeholder-gap when indices aren’t sequential from {{1}}) AND at send‑time (HandleSendInternal + HandleSendExternal return 400 too-many-parameters for >3 body parameters). Both template‑list endpoints emit a per‑template placeholders: {count, indices, bodyText, exampleValues} object and a top‑level maxPlaceholders: 3, so integrations build their parameters array without re‑parsing {{n}}. Meta accepts more but larger templates hurt WABA sender quality rating.
Per‑day new‑chat abuse quota
Configurable newChatsPerDayLimit — default 10, cap 10000, 0 = unlimited. Enforced atomically at send time on distinct outbound recipients per calendar day (UTC). Same‑day replies to open conversations don’t count. Applies to both send-external and the chat.html new‑chat flow. Per‑tenant counter file under App_Data/<tenant>/chat-whatsapp/.
Live 24‑hour customer service window pill
Every WA group’s header shows a live pill: green “Window open · Xh Ym left” (from the customer’s last inbound OR your last successful template send) or amber “Window closed · template required”. Ticks every 60 seconds. Hover reveals whether the window opened from an inbound message or from a template.
Compose bar auto‑swaps to template picker
When the window is closed, the normal text input is replaced by a helper line, a template picker populated from approved templates, per‑placeholder inputs with a live rendered preview, and a Send button. Meta’s rule that free‑form is blocked outside the window is never a surprise. On the customer’s next inbound the input flips back over WebSocket — no page reload.
Routing users auto‑receive inbound as email
Every routing user’s profile.email from the tenant’s sip‑credentials directory is auto‑added to the inbound‑forward list — deduped with the explicit CSV list operators have already configured. Adding a user to WA routing puts them on the email fan‑out automatically.
Expandable per‑event activity feed
Every row in the “Recent activity” panel on /whatsapp-admin.html is a native <details> element. Click to see full audit JSON — parsed request body, Meta response envelope, wamid, Meta error codes, kind:"text"|"template", template name. A Copy JSON button per row makes bug reports one click. Outbound rows now stamp the actual text sent or the template name, so admins see WHAT was sent, not just Meta’s response envelope.
wa.me click‑to‑chat
The recommended customer‑initiated entry point is a wa.me/<E.164> link on your website or email footer. The customer’s first message opens a fresh 24‑hour customer service window with no template required — the cheapest way to start a conversation.
How the loop works
Round-trip from a customer’s WhatsApp to your team’s chat UI and back — every hop stays on your own infrastructure.
Set it up in six steps
The whole thing takes 15–30 minutes end to end. Longer only if you have to complete Meta’s Business Verification (needed for publishing the app so real inbound flows).
- Create a Meta App.On developers.facebook.com/apps create a Business-type app and add the WhatsApp product. Note the App ID and App Secret.
- Get a Phone Number ID.WhatsApp Manager → Phone numbers. Use Meta’s assigned Test Number to start, or add your own number (requires Business Verification). Note the Phone number ID.
- Get an Access Token.For pilots: temporary 24h token from the API Setup panel. For production: Business Settings → System Users → Add → generate a permanent System User token with
whatsapp_business_messaging+whatsapp_business_managementpermissions. - Open our admin page.Sign in to your tenant and open
/whatsapp-admin.html. Paste App ID, App Secret, WABA ID, Phone Number ID, Access Token and Sender Phone. Save. - Configure the Meta webhook.Copy the Callback URL and Verify Token shown on the admin page into Meta App → WhatsApp → Configuration → Webhooks. Click Verify and Save. Subscribe to the
messagesfield. - Send a test message.From the admin page Test Send section, enter an E.164 recipient (must be on Meta’s allowed-tester list until the app is Published) and a body. Send. Reply arrives on the recipient’s WhatsApp within ~200 ms.
Detailed click-by-click walkthrough with the exact Meta console URLs is embedded in the tenant admin page — open /whatsapp-admin.html and read the Where do I find each value? card at the top.
vs. a WhatsApp BSP (Twilio, Vonage, MessageBird…)
Business Solution Providers hold the Meta relationship and see every message before forwarding it to you. Our connector is a direct-to-Meta bridge: you register your own Meta App, hold your own Access Token, and Meta posts webhooks straight to your server. Fewer parties, lower latency, no BSP fee.
| Property | Typical BSP | CodeB direct-to-Meta bridge |
|---|---|---|
| Who holds the Meta relationship | The BSP | You |
| Who sees your messages first | The BSP’s cloud | Your own IIS server |
| Where media is stored | BSP CDN + your systems | Your filesystem only, cached on first fetch |
| Per-message fee on top of Meta’s | Yes, usually per-message | Zero — included with CodeB |
| Multi-tenant credential isolation | BSP-account scoped | Per-tenant App_Data directory |
| Vendor lock-in on message archive | Their format, their portal | Plain JSON files on your disk |
| Webhook signature verification | Handled by BSP (opaque) | HMAC-SHA256 in your code, [RASP-*] audit |
| appsecret_proof on Graph calls | Sometimes optional | Always on, silently backward-compatible |
| Roll-your-own template management | Via BSP UI | Meta Dashboard directly (nothing between you and Meta) |
If you need enterprise BSP features (managed template gallery, hosted number provisioning, phone-tree flows), a BSP is the right tool. If you want sovereignty, direct billing, and every byte on your own infrastructure, use this.
Frequently asked
Is this WhatsApp bridge sovereign?
Yes. Each tenant supplies its own Meta App credentials. Every message and every media blob is stored on the tenant’s own IIS filesystem under App_Data. No CodeB-side relay, no third-party BSP in the middle. If you delete the tenant directory, every WhatsApp conversation with that tenant is gone.
How does it differ from Twilio or Vonage’s WhatsApp?
They’re Business Solution Providers — they hold the Meta relationship and see every message before forwarding it. Our connector is a direct-to-Meta bridge: you register your own Meta App, hold your own Access Token, and Meta posts webhooks straight to your server. Fewer parties, lower latency, no per-message BSP fee.
What does “appsecret_proof on every Graph call” mean?
Meta lets app owners require an HMAC-SHA256 proof (App Secret over the Access Token) on every server-to-server Graph API call. Even if an Access Token leaks, calls without the proof are rejected. Our connector always computes and sends the proof, so you can enable Meta’s “Require app secret” toggle any time without breaking anything.
Where does media go?
Inbound message with an image / audio / video / document / sticker: the connector fetches Meta’s media metadata, downloads the binary from the lookaside URL, and caches both under App_Data/<tenant>/chat-whatsapp/media/<mediaId>. Chat clients render inline via /whatsapp.ashx?action=media&id=<mediaId>, admin-gated. Nothing leaves your infrastructure after the initial Meta fetch.
What happens when Meta retries a webhook?
Every inbound message is written to chat/groups/wa-inbox/messages/wa-<msg-id>.json. Meta message IDs are globally unique. On retry the file already exists and the ingestion loop logs [WA-DEDUP] skip-duplicate instead of double-posting.
Can I use this before publishing the Meta App?
Outbound send works pre-publish as long as the recipient is on Meta’s allowed-tester list. Inbound, however, requires the Meta App to be Published (which needs Business Verification). While unpublished, Meta only delivers synthetic events fired from the Meta Dashboard’s Send test webhook button.
How much does it cost?
The connector is included at zero cost with CodeB Sovereign Communications. You pay Meta directly for WhatsApp conversations (see Meta’s WhatsApp Business Pricing page). No per-message CodeB fee, no BSP markup.
Which webhook fields should I subscribe to?
Only messages is required — that covers both inbound messages AND delivery / read / failed status callbacks for outbound messages. Recommended additional subscriptions: message_template_status_update (drives the WebSocket toasts to chat.html), phone_number_quality_update, account_alerts. Full field list with per-field handling is on the admin page.
Can I message a customer after 24 hours of silence?
Not with free‑form text — Meta blocks that outside the 24‑hour customer service window. Two options are supported. (1) Send a Meta‑approved message template (utility, authentication or marketing category, reviewed by Meta) — a successful template send reopens the window for 24 hours. The compose bar in chat.html auto‑swaps to the template picker when the header pill goes amber, so the operator is never surprised. (2) Publish a wa.me/<E.164> click‑to‑chat link and let the customer message you first — a customer‑initiated inbound opens a fresh window with no template required.
What happens if my API key leaks?
Two mitigations. First, keys are mint‑once — the plaintext value is shown once at generation, then masked forever — and compared in constant time, with a uniform 401 on any auth failure. Second, an API key can be template‑bound: pinned to exactly one approved template and one language. A leaked bound key can send only that Meta‑reviewed template — free‑form is refused with 403 template-required, other templates with 403 template-mismatch, wrong language with 403 template-language-mismatch. Blast radius from a leak is at most one reviewed template until the key is revoked from /whatsapp-admin.html. The per‑day new‑chat quota caps how many distinct recipients a leaked key can reach in a day even before revocation.
Do I need a Business Solution Provider (BSP)?
No. This connector talks straight to Meta’s WhatsApp Business Cloud API — you register your own Meta App, hold your own Access Token, and Meta posts webhooks directly to your IIS server. No BSP is in the middle, no per‑message BSP fee is charged, and no third party sees your messages before you do. A BSP is still the right tool if you specifically want a managed template gallery, hosted number provisioning, or phone‑tree flow builders — things Meta’s own Dashboard doesn’t ship. Everything else, including message templates, media, delivery receipts and the 24‑hour customer service window, works directly against Meta with no intermediary.
How many placeholders can a WhatsApp template have?
Up to 3 {{n}} placeholders per template body. The cap is enforced on BOTH template creation (rejected with 400 too-many-placeholders when the body has more than 3) AND at send‑time (rejected with 400 too-many-parameters when the outbound components’ body parameters array exceeds 3). Placeholders must also be sequential starting at {{1}} — a body with {{1}} and {{3}} but no {{2}} is rejected with 400 placeholder-gap on creation. Meta itself accepts up to about ten placeholders in a body, but templates with more than a handful get flagged as poor quality and hurt your WABA sender quality rating; 3 covers the common hospitality patterns (name, date, code / name, property, check‑in / etc.) without inviting quality‑rating drag. Both templates-list and templates-list-external report the cap as a top‑level maxPlaceholders field and each template’s own placeholders: {count, indices, bodyText, exampleValues} object, so integrations know exactly how many parameters to send without re‑parsing the body text.
Who builds this?
Aloaha Limited in Malta. Same team that builds the rest of CodeB Sovereign Communications.
Ready to plug your Meta App in?
The admin page walks you through every value; the Meta Dashboard values panel shows exactly what to paste where.