Skip to main content
Vendor ConnectIntegrationWhatsApp APIMeta PageEmbedded Signup

WhatsPortal Vendor Connect — Integration Guide

Complete step-by-step guide for vendors to embed WhatsApp Business Account onboarding and Facebook Page connection 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 or Facebook Pages from inside your platform — without ever visiting the WhatsPortal dashboard.

WhatsPortal supports two integration types, both using the same API key and the same session-based flow:

Integration What the customer does What you receive
WhatsApp Business API Completes Meta Embedded Signup to authorize their WhatsApp Business account account_id, waba_id, phone_number_id
Meta Page (Facebook Pages) Logs in with Facebook and selects which Pages to connect Array of connected pages with id, name, category

The entire integration takes under 10 minutes of development time per flow.


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

Every integration has three parts regardless of type:

  1. Backend — Your server calls POST /api/vendor/sessions to create a session token and get a connect_url.
  2. Frontend — Your frontend opens the connect_url in a new browser tab.
  3. Frontend — Your frontend listens for a postMessage from the tab with the result.

The only difference between WhatsApp and Meta Page is the integrationType field you pass in step 1, and the shape of the result you receive in step 3.


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",
  "externalId": "customer_123",
  "origin": "https://app.yourproduct.com",
  "integrationType": "whatsapp",
  "state": "csrf_token_or_any_value_you_want_echoed_back"
}
Field Required Description
workspaceName Yes Display name for the workspace. Usually your customer's business name. Max 100 characters.
externalId Yes Your stable ID for this customer (user ID, org ID, etc.). Used for idempotency — calling again with the same externalId reuses the existing workspace. 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
integrationType No "whatsapp" (default) or "meta_page". Determines which connect page opens and which flow runs.
state No Any opaque string. Echoed back in the postMessage payload. Use it for CSRF protection.

Response

{
  "session_token": "a3f9...64-char-hex-string...1c2d",
  "connect_url": "/vendor/connect/whatsapp?session_token=a3f9...1c2d"
}
Field Description
session_token The raw token. Valid for 10 minutes.
connect_url The full path to open in a new tab. For whatsapp/vendor/connect/whatsapp?session_token=.... For meta_page/vendor/connect/meta-page?session_token=.... Always use this URL — don't construct it manually.

Example (Node.js)

// WhatsApp session
app.post('/api/whatsapp-session', async (req, res) => {
  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}`,
      externalId: String(customerId),
      origin: 'https://app.yourproduct.com',
      integrationType: 'whatsapp',
      state: generateCsrfToken(),
    }),
  })

  const data = await response.json()
  if (!response.ok) return res.status(500).json({ error: data.error })
  res.json({ connect_url: data.connect_url })
})

// Meta Page session — same endpoint, different integrationType
app.post('/api/meta-page-session', async (req, res) => {
  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}`,
      externalId: String(customerId),   // same externalId → same workspace
      origin: 'https://app.yourproduct.com',
      integrationType: 'meta_page',
      state: generateCsrfToken(),
    }),
  })

  const data = await response.json()
  if (!response.ok) return res.status(500).json({ error: data.error })
  res.json({ connect_url: data.connect_url })
})

Tip: Using the same externalId for both WhatsApp and Meta Page sessions connects both to the same WhatsPortal workspace. This is the recommended pattern — one workspace per customer, multiple integrations within it.

Error Responses

Status Meaning
401 API key is missing, invalid, or your vendor account is inactive.
400 origin not in your allowedOrigins, or externalId/workspaceName failed validation.
500 Server error 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:

  1. User clicks button → synchronously open the tab (the tab shows a loading state while you fetch the session).
  2. In the background, fetch the session from your backend and navigate the tab to the final URL.

Here is the complete, production-ready integration snippet. It works for both integration types — just swap the backend endpoint:

const WHATSPORTAL_ORIGIN = 'https://www.whatsportal.io'

/**
 * @param {string} sessionEndpoint - Your backend endpoint that returns { connect_url }
 *   e.g. '/api/whatsapp-session' or '/api/meta-page-session'
 * @param {function} onSuccess - Called with the postMessage result on success
 * @param {function} onAbandoned - Called when the user closes the tab without completing
 */
async function initiateVendorConnect(sessionEndpoint, onSuccess, onAbandoned) {
  // Fetch session + connect_url from YOUR backend first
  const { connect_url } = await fetch(sessionEndpoint).then(r => r.json())

  // Open the tab SYNCHRONOUSLY from the click handler
  const tab = window.open(
    `${WHATSPORTAL_ORIGIN}${connect_url}`,
    '_blank'
  )

  if (!tab) {
    showError('Please allow popups for this site and try again.')
    return
  }

  // Detect tab closed without completing
  const abandonTimer = setInterval(() => {
    if (tab.closed) {
      clearInterval(abandonTimer)
      onAbandoned()
    }
  }, 500)

  // Listen for result
  function handleMessage(event) {
    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') {
      onSuccess(event.data)
    }
  }

  window.addEventListener('message', handleMessage)
}

// WhatsApp button
document.getElementById('connect-whatsapp-btn').addEventListener('click', () => {
  void initiateVendorConnect(
    '/api/whatsapp-session',
    (data) => console.log('WhatsApp connected:', data.account_id),
    ()     => console.log('WhatsApp connect abandoned'),
  )
})

// Meta Page button
document.getElementById('connect-meta-page-btn').addEventListener('click', () => {
  void initiateVendorConnect(
    '/api/meta-page-session',
    (data) => console.log('Pages connected:', data.pages),
    ()     => console.log('Meta Page connect abandoned'),
  )
})

Pre-fetch pattern (zero-delay tab open)

If you want the tab to open instantly on click, pre-warm the session token on hover:

let prefetchedConnectUrl = null

connectBtn.addEventListener('mouseenter', async () => {
  if (prefetchedConnectUrl) return
  const { connect_url } = await fetch('/api/whatsapp-session').then(r => r.json())
  prefetchedConnectUrl = connect_url
})

connectBtn.addEventListener('click', () => {
  const url = prefetchedConnectUrl ?? ''
  prefetchedConnectUrl = null  // consume it; next hover prefetches fresh

  const tab = window.open(`${WHATSPORTAL_ORIGIN}${url}`, '_blank')
  // ... rest of listener setup
})

Part 3 — Handle the Result

WhatsApp success payload

{
  type:            'whatsportal:connect:success',
  account_id:      string,   // Stable WhatsPortal workspace ID — store this permanently
  waba_id:         string,   // Meta WhatsApp Business Account ID
  phone_number_id: string,   // Meta Phone Number ID
  state?:          string,   // Echoed from your session `state` parameter
}

Meta Page success payload

{
  type:   'whatsportal:connect:success',
  pages:  Array<{
    id:       string,   // Facebook Page ID
    name:     string,   // Page display name
    category: string,   // e.g. "Software", "Local business"
  }>,
  state?: string,       // Echoed from your session `state` parameter
}

Note: The type field is always whatsportal:connect:success for both flows. Distinguish them by checking which fields are present: WhatsApp results have account_id, Meta Page results have pages.

Handling both in one listener

function handleMessage(event) {
  if (event.origin !== WHATSPORTAL_ORIGIN) return
  if (event.data?.type !== 'whatsportal:connect:success') return

  if (event.data.account_id) {
    // WhatsApp connect
    const { account_id, waba_id, phone_number_id, state } = event.data
    verifyState(state)
    await saveWhatsAppConnection({ account_id, waba_id, phone_number_id })

  } else if (event.data.pages) {
    // Meta Page connect
    const { pages, state } = event.data
    verifyState(state)
    await savePageConnections(pages)
  }
}

The account_id

For WhatsApp connections, the account_id is a stable, permanent identifier for this customer's WhatsApp Business workspace. Store it in your database alongside the customer record.

Idempotency guarantee: If the same customer goes through the connect flow again with the same externalId, they receive the same account_id. This is true for both WhatsApp and Meta Page sessions — the workspace is always the same for a given externalId.


React / Next.js Example

import { useState, useCallback } from 'react'

const WHATSPORTAL_ORIGIN = 'https://www.whatsportal.io'

type IntegrationType = 'whatsapp' | 'meta_page'

function useVendorConnect(type: IntegrationType) {
  const [status, setStatus] = useState<'idle' | 'connecting' | 'success' | 'error'>('idle')

  const handleClick = useCallback(async () => {
    const endpoint = type === 'whatsapp' ? '/api/whatsapp-session' : '/api/meta-page-session'
    const { connect_url } = await fetch(endpoint).then(r => r.json())

    const tab = window.open(`${WHATSPORTAL_ORIGIN}${connect_url}`, '_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 !== 'whatsportal:connect:success') return

      clearInterval(abandonTimer)
      window.removeEventListener('message', handleMessage)
      setStatus('success')

      if (type === 'whatsapp') {
        void fetch('/api/save-whatsapp', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ account_id: event.data.account_id }),
        })
      } else {
        void fetch('/api/save-pages', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ pages: event.data.pages }),
        })
      }
    }

    window.addEventListener('message', handleMessage)
  }, [type])

  return { status, handleClick }
}

export function ConnectWhatsAppButton() {
  const { status, handleClick } = useVendorConnect('whatsapp')
  return (
    <button onClick={() => void handleClick()} disabled={status === 'connecting'}>
      {status === 'idle' && 'Connect WhatsApp Business'}
      {status === 'connecting' && 'Connecting...'}
      {status === 'success' && 'Connected ✓'}
      {status === 'error' && 'Try Again'}
    </button>
  )
}

export function ConnectMetaPageButton() {
  const { status, handleClick } = useVendorConnect('meta_page')
  return (
    <button onClick={() => void handleClick()} disabled={status === 'connecting'}>
      {status === 'idle' && 'Connect Facebook Pages'}
      {status === 'connecting' && 'Connecting...'}
      {status === 'success' && 'Connected ✓'}
      {status === 'error' && 'Try Again'}
    </button>
  )
}

Retry & Failure Scenarios

Scenario What happens What you should do
Customer closes tab without completing abandonTimer detects tab.closed Show a retry button. Request a fresh session.
Session token expires (10 min) Tab shows "Session has expired" message Create a new session token and open a new tab.
window.open returns null Browser blocked the popup Show error: "Please allow popups for this site."
Network error during Meta OAuth Tab shows an error toast; customer can retry Session remains valid; same tab can be retried.
Customer denies Facebook permissions Tab returns to ready state (no postMessage sent) Your abandonTimer will eventually fire when they close the tab.

Security Checklist

Before going live, verify every item below.

Check Why it matters
API key is in a server-side environment variable 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. Validate it, but don't use it to identify users — use account_id or pages instead.
account_id is stored server-side 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.

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 Meta OAuth.
  3. After a successful WhatsApp connect, verify:
    • account_id, waba_id, phone_number_id arrive in your postMessage handler.
    • Calling the session endpoint again with the same externalId returns a new token but resolves to the same account_id.
  4. After a successful Meta Page connect, verify:
    • pages array arrives with id, name, category for each connected page.
    • The same externalId used for WhatsApp also maps to the same workspace.

Testing checklist

WhatsApp:

  • Tab opens and shows your vendor logo and name
  • Meta Embedded Signup popup appears
  • Success postMessage arrives with account_id, waba_id, phone_number_id
  • state is echoed back correctly
  • Closing without completing triggers the abandon handler
  • Second session with same externalId returns the same account_id

Meta Page:

  • Tab opens and shows your vendor logo and name
  • Facebook login popup appears with page selection
  • Success postMessage arrives with pages array
  • state is echoed back correctly
  • Closing without completing triggers the abandon handler
  • Sessions for WhatsApp and Meta Page with the same externalId refer to the same workspace

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 connect_url to the frontend.


Can a customer connect both WhatsApp and a Facebook Page?

Yes — use the same externalId for both session calls. Both integrations will be connected to the same WhatsPortal workspace. The customer goes through two separate flows (two button clicks, two tabs), but the result is a single workspace with both integrations active.


What if the customer's session token expires before they complete the flow?

The tab will show an "expired" message. Offer a retry button that calls your backend for a fresh session token and opens a new tab.


Will calling the same externalId twice create two workspaces?

No. WhatsPortal uses the externalId to deduplicate. The first call creates the workspace; every subsequent call with the same externalId reuses 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_wa_1, customer_123_wa_2).


Can I use a popup instead of a new tab?

No. The WhatsApp connect tab uses Meta's Embedded Signup which calls FB.login() internally. The Meta Page connect tab also calls FB.login() directly. FB.login() opens a popup of its own. Most browsers (especially Safari) block nested popups. Opening WhatsPortal as a new tab (no width/height in window.open) avoids this entirely.


Do my customers need a WhatsPortal account?

No. 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.


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)",
  "integrationType": "whatsapp" | "meta_page"  (optional, default: "whatsapp"),
  "state":           "string (optional, echoed back in postMessage)"
}

Response 200:
{
  "session_token": "64-char hex",
  "connect_url":   "/vendor/connect/whatsapp?session_token=..."
                   "/vendor/connect/meta-page?session_token=..."
}

Connect tab URLs

Type URL
WhatsApp https://www.whatsportal.io/vendor/connect/whatsapp?session_token=<token>
Meta Page https://www.whatsportal.io/vendor/connect/meta-page?session_token=<token>

Open with window.open(url, '_blank') — no width/height.

postMessage success shapes

// WhatsApp
{
  type:            'whatsportal:connect:success',
  account_id:      'wp_...',   // store this permanently
  waba_id:         '...',
  phone_number_id: '...',
  state:           '...'       // only present if you passed state
}

// Meta Page
{
  type:   'whatsportal:connect:success',
  pages:  [{ id: '...', name: '...', category: '...' }],
  state:  '...'                // only present if you passed 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