WhatsPortal Vendor Connect — Integration Guide
Complete step-by-step guide for vendors to embed WhatsApp Business Account onboarding directly inside their own product using the WhatsPortal Vendor Connect API.
This guide walks you through integrating WhatsPortal Vendor Connect into your product. When you are done, your customers will be able to connect their WhatsApp Business account from inside your platform — without ever visiting the WhatsPortal dashboard.
The entire flow takes under 10 minutes of development time.
What You Are Building
Your customer clicks a button inside your product. A new browser tab opens showing your brand's logo and name. The customer completes the WhatsApp Business API authorization in a few clicks. The tab closes automatically. Your app receives a stable account_id that permanently identifies their connected WhatsApp account.
Your product WhatsPortal tab Meta OAuth
────────── ────────────── ──────────
Click "Connect" button
→ Your backend creates
a session (API call)
→ Your frontend opens ──────▶ Shows your branding
a new tab "Connect WhatsApp Business"
button visible
◀── User clicks
Meta login popup
User authorizes
Tab closes ◀─────────────────────────────
◀── postMessage arrives
{ account_id, waba_id,
phone_number_id }
Store account_id in your DB
Before You Start
Contact the WhatsPortal team to receive:
| Item | Description |
|---|---|
| API Key | A secret 64-character hex key for your server. Never expose this in frontend code. |
| Vendor ID | Your unique vendor identifier (for reference, not used in API calls directly). |
You will also need to tell us your allowed origin(s) — the exact URL(s) your frontend runs on, e.g. https://app.yourproduct.com. We whitelist these server-side. The postMessage result will only ever be sent to one of your registered origins.
Integration Overview
The integration has three parts:
- Backend — Your server calls
POST /api/vendor/sessionsto create a session token. - Frontend — Your frontend opens the WhatsPortal connect tab in a new browser tab using the token.
- Frontend — Your frontend listens for a
postMessagefrom the tab with the result.
Part 1 — Backend: Create a Session
Your backend calls the WhatsPortal session endpoint before the user clicks the button. The session token is short-lived (10 minutes) and single-use.
Endpoint
POST https://www.whatsportal.io/api/vendor/sessions
Authorization: Bearer <your_api_key>
Content-Type: application/json
Request Body
{
"workspaceName": "Acme Co WhatsApp",
"externalId": "customer_123",
"origin": "https://app.yourproduct.com",
"state": "csrf_token_or_any_value_you_want_echoed_back"
}
| Field | Required | Description |
|---|---|---|
workspaceName |
Yes | Display name for the WhatsApp workspace. Usually your customer's business name. Max 100 characters. |
externalId |
Yes | Your stable ID for this customer. Could be their user ID, org ID, etc. Used for idempotency — calling again with the same externalId will reuse the existing workspace, not create a duplicate. Max 64 characters, alphanumeric + hyphens/underscores/dots/spaces. No /. |
origin |
Yes | The exact scheme + host of the page that will listen for the postMessage. Must match one of your registered allowedOrigins. Example: https://app.yourproduct.com |
state |
No | Any opaque string. It will be echoed back to you in the postMessage payload. Use it to verify the response came from a session you initiated (CSRF protection). |
Response
{
"session_token": "a3f9...64-char-hex-string...1c2d"
}
The token is valid for 10 minutes from creation. Create it right before the user is about to click the button — not on page load.
Example (Node.js / Express)
app.post('/api/whatsapp-session', async (req, res) => {
// Identify the current user/org from your own auth
const { customerId, customerName } = req.user
const response = await fetch('https://www.whatsportal.io/api/vendor/sessions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.WHATSPORTAL_API_KEY}`,
},
body: JSON.stringify({
workspaceName: `${customerName} WhatsApp`,
externalId: String(customerId),
origin: 'https://app.yourproduct.com',
state: generateCsrfToken(), // your own CSRF token
}),
})
if (!response.ok) {
const err = await response.json()
return res.status(500).json({ error: err.error })
}
const { session_token } = await response.json()
res.json({ session_token })
})
Error Responses
| Status | Meaning |
|---|---|
401 |
API key is missing, invalid, or your vendor account is inactive. |
400 |
origin is not in your allowedOrigins list, or externalId/workspaceName failed validation. |
500 |
Firestore write failure on our side. Retry once. |
Part 2 — Frontend: Open the Connect Tab
Critical rule: window.open must be called synchronously inside a click event handler. Never await anything before calling window.open — browsers treat async-initiated window.open calls as programmatic popups and block them.
The correct pattern is:
- User clicks button → synchronously open the tab with a loading state.
- In parallel (or just before the click), fetch the session token from your backend.
Here is the complete, production-ready integration snippet:
// Paste this in your frontend and wire up the button below.
const WHATSPORTAL_ORIGIN = 'https://www.whatsportal.io'
async function initiateWhatsAppConnect() {
// === STEP 1: Fetch session token from YOUR backend ===
// Do this before the user clicks if possible, or show an inline loader
// while the tab is already open (see pattern below).
const { session_token } = await fetch('/api/whatsapp-session')
.then(r => r.json())
// === STEP 2: Open the connect tab SYNCHRONOUSLY ===
// This call must happen inside a user-initiated event (click).
// No width/height = new tab (not popup). Required to allow FB.login() inside.
const tab = window.open(
`${WHATSPORTAL_ORIGIN}/vendor/connect?session_token=${session_token}`,
'_blank'
)
if (!tab) {
// window.open returned null = browser blocked the tab.
// This is rare when called from a direct click handler.
showError('Please allow popups for this site and try again.')
return
}
// === STEP 3: Detect if the user closes the tab without completing ===
const abandonTimer = setInterval(() => {
if (tab.closed) {
clearInterval(abandonTimer)
onAbandoned() // update your UI to show "Cancelled"
}
}, 500)
// === STEP 4: Listen for the result postMessage ===
function handleMessage(event) {
// Always validate the origin. Never skip this check.
if (event.origin !== WHATSPORTAL_ORIGIN) return
// Only handle WhatsPortal Vendor Connect messages
if (!event.data?.type?.startsWith('whatsportal:connect:')) return
clearInterval(abandonTimer)
window.removeEventListener('message', handleMessage)
if (event.data.type === 'whatsportal:connect:success') {
onSuccess(event.data)
} else {
onError(event.data)
}
}
window.addEventListener('message', handleMessage)
}
// Wire to a button. NEVER call initiateWhatsAppConnect() on page load.
document.getElementById('connect-whatsapp-btn')
.addEventListener('click', () => void initiateWhatsAppConnect())
The better pattern: pre-fetch token, open tab instantly
If you want the tab to open with zero delay on click, pre-fetch the session token when the user hovers over or focuses the button:
let prefetchedToken = null
// Pre-warm on hover (optional but recommended)
connectBtn.addEventListener('mouseenter', async () => {
if (prefetchedToken) return
const { session_token } = await fetch('/api/whatsapp-session').then(r => r.json())
prefetchedToken = session_token
})
connectBtn.addEventListener('click', () => {
const token = prefetchedToken ?? '' // fallback: open tab then it shows loading
prefetchedToken = null // consume it; next click gets a fresh one
const tab = window.open(
`https://www.whatsportal.io/vendor/connect?session_token=${token}`,
'_blank'
)
// ... rest of listener setup
})
Part 3 — Frontend: Handle the Result
When the customer completes WhatsApp authorization, the tab sends a postMessage to your page and then closes itself.
Success payload
{
type: 'whatsportal:connect:success'
account_id: string // Stable WhatsPortal workspace ID. Store this in your DB.
waba_id: string // Meta WhatsApp Business Account ID
phone_number_id: string // Meta Phone Number ID
state?: string // Echoed back from your session creation `state` parameter
}
Error payload
{
type: 'whatsportal:connect:error'
error: string // Human-readable description
state?: string // Echoed back from your session creation `state` parameter
}
Handling the result
function onSuccess(data) {
const { account_id, waba_id, phone_number_id, state } = data
// 1. Verify the state matches what you sent (CSRF check)
if (state !== expectedState) {
console.error('State mismatch — possible replay attack')
return
}
// 2. Store account_id in your database against this customer
await fetch('/api/save-whatsapp-connection', {
method: 'POST',
body: JSON.stringify({ account_id, waba_id, phone_number_id }),
})
// 3. Update your UI
showSuccessMessage('WhatsApp Business connected!')
}
function onError(data) {
console.error('WhatsApp connect failed:', data.error)
showError('Connection failed. Please try again.')
}
function onAbandoned() {
showMessage('WhatsApp connection was cancelled.')
}
The account_id
The account_id returned in the success payload is a stable, permanent identifier for this customer's WhatsApp Business workspace. Store it in your database alongside the customer record.
You will use account_id in future API calls to WhatsPortal (analytics, template sending, etc., in future API phases).
Idempotency guarantee: If the same customer goes through the connect flow again (e.g. to reconnect after a disconnect), they will receive the same account_id as long as you pass the same externalId in the session creation call.
Retry & Failure Scenarios
| Scenario | What happens | What you should do |
|---|---|---|
| Customer closes tab without completing | abandonTimer detects tab.closed, calls onAbandoned() |
Show a retry button. Call /api/whatsapp-session again for a fresh token. |
| Session token expires (10 min) | Tab shows "Session has expired" message | Create a new session token and open the tab again. |
| Customer already connected this WhatsApp number elsewhere | Tab shows toast: "This number is already registered" | The customer needs to disconnect it from the other account first. |
window.open returns null |
Browser blocked the popup | Show error: "Please allow popups for this site and try again." |
| Network error during Meta OAuth | Tab shows an error toast, customer can retry | The session remains valid; the customer can click Connect again in the same tab. |
React / Next.js Example
import { useState, useCallback } from 'react'
const WHATSPORTAL_ORIGIN = 'https://www.whatsportal.io'
export function ConnectWhatsAppButton({ customerId }: { customerId: string }) {
const [status, setStatus] = useState<'idle' | 'connecting' | 'success' | 'error'>('idle')
const handleClick = useCallback(async () => {
// Fetch token immediately — open tab in the same tick
// (browsers allow window.open in async functions called from click handlers
// as long as user interaction is recent and no awaits precede the open call)
const tokenPromise = fetch('/api/whatsapp-session').then(r => r.json())
// Get token first, then open (acceptable in most browsers from a direct click)
const { session_token } = await tokenPromise
const tab = window.open(
`${WHATSPORTAL_ORIGIN}/vendor/connect?session_token=${session_token}`,
'_blank'
)
if (!tab) {
setStatus('error')
return
}
setStatus('connecting')
const abandonTimer = setInterval(() => {
if (tab.closed) {
clearInterval(abandonTimer)
setStatus('idle')
}
}, 500)
function handleMessage(event: MessageEvent) {
if (event.origin !== WHATSPORTAL_ORIGIN) return
if (!event.data?.type?.startsWith('whatsportal:connect:')) return
clearInterval(abandonTimer)
window.removeEventListener('message', handleMessage)
if (event.data.type === 'whatsportal:connect:success') {
setStatus('success')
// Save account_id to your backend
void fetch('/api/save-whatsapp-connection', {
method: 'POST',
body: JSON.stringify({ account_id: event.data.account_id }),
headers: { 'Content-Type': 'application/json' },
})
} else {
setStatus('error')
}
}
window.addEventListener('message', handleMessage)
}, [customerId])
return (
<button onClick={() => void handleClick()} disabled={status === 'connecting'}>
{status === 'idle' && 'Connect WhatsApp Business'}
{status === 'connecting' && 'Connecting...'}
{status === 'success' && 'Connected ✓'}
{status === 'error' && 'Try Again'}
</button>
)
}
Security Checklist
Before going live, verify every item below.
| Check | Why it matters |
|---|---|
| API key is in an environment variable on your server | If it leaks to the frontend, anyone can create sessions under your vendor account. |
event.origin !== 'https://www.whatsportal.io' check is in place |
Without this, a malicious page could send a fake postMessage to your window. |
state parameter is a random CSRF token, not a user ID |
The state is echoed back unauthenticated. Validate it, but don't use it to identify users — use the account_id instead. |
account_id is stored server-side, not just in localStorage |
The account_id is your permanent reference. It must survive page reloads and device changes. |
allowedOrigins registered with WhatsPortal is HTTPS |
Never register an http:// origin for production. |
window.open is called from inside a click handler |
If called programmatically (outside a user gesture), browsers block it. Always wire it to a button click. |
Testing Your Integration
Local development
- Register
http://localhost:3000(or your local port) as an allowed origin with your WhatsPortal account manager. - Use a real Meta business account in test mode. WhatsPortal does not provide a sandbox environment for the Meta OAuth step — the embedded signup requires a real Facebook account.
- After a successful connect, verify:
- The
account_idarrives in yourpostMessagehandler. - The tab closes automatically.
- Calling the session endpoint again with the same
externalIdreturns a new token but eventually resolves to the sameaccount_id(idempotency).
- The
Checklist
- Button opens a new tab (not a popup with width/height).
- Tab shows your vendor logo and name.
- Meta OAuth popup appears when customer clicks Connect.
- Success
postMessagearrives withaccount_id,waba_id,phone_number_id. -
stateparameter is echoed back correctly. - Closing the tab without completing triggers the abandon handler.
- Calling the session endpoint twice with the same
externalIdreturns the sameaccount_id. - Revoking
allowedOriginsfrom our side immediately blocks new session creation.
Frequently Asked Questions
Can I call POST /api/vendor/sessions from the frontend?
No. The endpoint requires your API key in the Authorization header. Your API key must never be in frontend JavaScript. Always call this from your backend and return only the session_token to the frontend.
What if the customer's session token expires before they complete the flow?
The tab will show an "expired" message. Your app should offer a retry button that calls your backend again for a fresh session token and opens a new tab.
Will calling the same externalId twice create two WhatsApp workspaces?
No. WhatsPortal uses the externalId to deduplicate. The first call creates the workspace. Every subsequent call with the same externalId reuses the existing workspace and returns a new session token pointing to it. The account_id you receive will always be the same for a given externalId.
Can one customer connect multiple WhatsApp numbers?
Each externalId maps to one workspace. To support multiple numbers per customer, use distinct externalId values (e.g. customer_123_number_1, customer_123_number_2).
Can I use a popup instead of a new tab?
No. The connect tab uses Meta's Embedded Signup which calls FB.login() internally. FB.login() opens a popup of its own. Most browsers (especially Safari) block nested popups — a popup opened by a popup is treated as a programmatic popup and silently suppressed. Opening WhatsPortal as a new tab (no width/height in window.open) avoids this entirely.
Do my customers need a WhatsPortal account?
No. The entire flow is embedded. Your customers interact only with the WhatsPortal connect tab (branded as your product) and Meta's OAuth dialog. They never sign up for or log into WhatsPortal directly.
What data does WhatsPortal store about my customers?
WhatsPortal stores the WhatsApp Business account credentials (WABA ID, phone number ID, long-lived access token) needed to send and receive messages on the customer's behalf. It does not store any personal customer data beyond what is returned by Meta's API. Refer to the WhatsPortal DPA (Data Processing Agreement) for full details.
Is there a webhook alternative to postMessage?
Not in the current phase. postMessage is the primary delivery mechanism. A server-side webhook option (for headless or mobile integrations) is on the roadmap.
Quick Reference
Session endpoint
POST https://www.whatsportal.io/api/vendor/sessions
Authorization: Bearer <api_key>
Content-Type: application/json
{
"workspaceName": "string (required, max 100 chars)",
"externalId": "string (required, max 64 chars, no /)",
"origin": "string (required, must match allowedOrigins)",
"state": "string (optional, echoed back in postMessage)"
}
Response 200:
{ "session_token": "64-char hex" }
Connect tab URL
https://www.whatsportal.io/vendor/connect?session_token=<token>
Open with window.open(url, '_blank') — no width/height.
postMessage success shape
{
type: 'whatsportal:connect:success',
account_id: 'wp_...', // store this permanently
waba_id: '...',
phone_number_id: '...',
state: '...' // only present if you passed state
}
postMessage error shape
{
type: 'whatsportal:connect:error',
error: 'human-readable message',
state: '...'
}
For integration support, reach out to the WhatsPortal partnerships team. Include your vendor ID and a description of the issue.
Start messaging smarter
WhatsPortal makes WhatsApp Business API simple for your whole team.
Get started free