Skip to main content
Vendor ConnectIntegrationWhatsApp APIEmbedded Signup

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.

Author

WhatsPortal

14 min read

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:

  1. Backend — Your server calls POST /api/vendor/sessions to create a session token.
  2. Frontend — Your frontend opens the WhatsPortal connect tab in a new browser tab using the token.
  3. Frontend — Your frontend listens for a postMessage from 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:

  1. User clicks button → synchronously open the tab with a loading state.
  2. 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

  1. Register http://localhost:3000 (or your local port) as an allowed origin with your WhatsPortal account manager.
  2. 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.
  3. After a successful connect, verify:
    • The account_id arrives in your postMessage handler.
    • The tab closes automatically.
    • Calling the session endpoint again with the same externalId returns a new token but eventually resolves to the same account_id (idempotency).

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 postMessage arrives with account_id, waba_id, phone_number_id.
  • state parameter is echoed back correctly.
  • Closing the tab without completing triggers the abandon handler.
  • Calling the session endpoint twice with the same externalId returns the same account_id.
  • Revoking allowedOrigins from 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