Embed member consent

Collect SMS/email opt-in and phone numbers in a BoomTower-hosted form. Open it in a popup (recommended) or embed it in an iframe. Your app owns authentication; BoomTower stores the member, organization membership, and consent record.

How it works

  1. Your server creates a short-lived embed session using your organization API key (X-API-Key).
  2. Your web app opens the returned URL in a popup (recommended) or an iframe.
  3. The user completes a consent form (name, phone, required checkboxes).
  4. BoomTower posts a postMessage to your app (parent window or popup opener) with completed, cancelled, or error. If that message is missed, poll the consent status API when the popup closes.

Prerequisites

  • A BoomTower organization and an API key attached to that org.
  • A stable end-user id in your product (send it as externalId).
  • For iframe embeds only: your domain must be on BoomTower's frame-ancestors allowlist (see “Deployment” below). Popups do not require this.

Organization id in URL paths

Use the bare organization ksuid (27 characters) in every /v1/organizations/{id}/… path. This is the id field from GET /v1/organizations or GET /v1/organizations/{id}.

Example: 3EXrgGUEVAI8xMFzT3j7IKo9ha5

Do not put the full BoomID string in the path (for example [btprod:organization]-[3EXrgGUEVAI8xMFzT3j7IKo9ha5]). Some endpoints accept that form for backwards compatibility, but ksuid-only is the supported contract.

Note: X-API-Key header values are different — those are always the full BoomID string for the apiKey type.

1) Create an embed session (server-side)

Call this from your backend only. Do not expose X-API-Key to the browser.

POST https://api.boomtower.com/v1/organizations/3EXrgGUEVAI8xMFzT3j7IKo9ha5/members/consent-embed-sessions
X-API-Key: YOUR_ORG_API_KEY
Content-Type: application/json

{
  "data": {
    "externalApp": "partnerapp",
    "externalId": "YOUR_USER_ID",
    "firstName": "Avery",
    "lastName": "Smith",
    "email": "avery@example.com",
    "phone": "+15551234567",
    "allowedOrigin": "https://app.yourdomain.com",
    "consentTextVersion": "2026-06-02_v1"
  }
}

Response (201) returns a token and an embed path:

{
  "success": true,
  "code": 201,
  "data": {
    "token": "2abc…",
    "expiresAt": "2026-06-02T15:30:00Z",
    "embedPath": "/embed/consent?token=2abc…"
  }
}

Token behavior: sessions expire after 30 minutes and become single-use after a successful submit.

Build the full URL as https://boomtower.com + embedPath.

2) Open the form (popup — recommended)

Popups avoid iframe CSP allowlisting on the BoomTower side and work the same way with postMessage. Center the window to feel like a modal.

const popup = window.open(
  embedUrl,
  'boomtower-consent',
  'width=440,height=640,left=200,top=100',
);

// Optional: detect when the user closes the popup without completing (see fallbacks below).
const pollClose = setInterval(() => {
  if (popup?.closed) {
    clearInterval(pollClose);
    void refreshConsentStatus(); // server-side GET — see section 5
  }
}, 500);

3) Embed the form (iframe — optional)

Use an iframe only if you need the form inline. Each partner domain must be allowlisted in BoomTower's frame-ancestors CSP (see “Deployment”).

const [embedUrl, setEmbedUrl] = useState<string | null>(null);

// After your server returns session.embedPath + website base:
setEmbedUrl(`https://boomtower.com${embedPath}`);

// In JSX:
{embedUrl && (
  <dialog open className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
    <div className="w-full max-w-md rounded-xl bg-white p-2 shadow-xl">
      <iframe
        title="Messaging preferences"
        src={embedUrl}
        className="h-[560px] w-full rounded-lg border-0"
        allow="clipboard-write"
      />
    </div>
  </dialog>
)}

4) Listen for completion (postMessage)

The embed page posts to your app when consent is saved, cancelled, or fails. Always validate event.origin — BoomTower may be served from more than one host (for example www.boomtower.com vs boomtower.com). Accept every production host you use in embedPath.

const BOOMTOWER_WEB_ORIGINS = new Set([
  'https://www.boomtower.com',
  'https://boomtower.com',
  'https://app.boomtower.com',
  // dev / preview:
  // 'https://dev.boomtower.com',
  // 'http://localhost:3000',
]);

function isBoomTowerOrigin(origin: string): boolean {
  return BOOMTOWER_WEB_ORIGINS.has(origin);
}

useEffect(() => {
  function onMessage(event: MessageEvent) {
    if (!isBoomTowerOrigin(event.origin)) return;
    if (event.data?.type !== 'boomtower:member-consent') return;

    const { status, memberId, organizationId, createdMember, error } = event.data;

    if (status === 'completed') {
      // Persist memberId in your app, close modal/popup, refresh UI.
      console.log('Consent saved', { memberId, organizationId, createdMember });
    } else if (status === 'cancelled') {
      console.log('User cancelled');
    } else if (status === 'error') {
      console.error('Consent error', error);
    }
  }

  window.addEventListener('message', onMessage);
  return () => window.removeEventListener('message', onMessage);
}, []);

Payload

  • type: boomtower:member-consent
  • status: completed | cancelled | error
  • memberId: present on completed
  • organizationId: present on completed

allowedOrigin: pass your app's exact origin when creating the embed session (for example https://app.yourdomain.com). BoomTower uses it as the postMessage target so messages are not broadcast to *.

If you only check a single BoomTower origin on the listener side, messages can be silently ignored even when consent saved successfully — include every BoomTower host listed above.

5) Fallback when postMessage is missed

Treat postMessage as the primary path, but add a server-side check for cases where the popup closes before the message is delivered (browser extensions, strict origin checks, or the user closing the window immediately after submit).

When the popup closes — or on a short delay after opening — call GET /v1/organizations/{id}/members/consent with the same externalApp and externalId you used to create the session. If a member record exists, persist memberId in your app and close your UI.

async function refreshConsentStatus() {
  const res = await fetch('/api/your-backend/consent-status'); // proxies GET …/members/consent
  const body = await res.json();
  const memberId = body?.data?.memberId;
  if (memberId) {
    // Link user to BoomTower member, dismiss dialog.
  }
}

As a last resort, some apps expose a manual “paste BoomTower member ID” field for support cases. Prefer the API lookup above — memberId is returned by both the embed submit response and the consent status endpoint.

Verify consent later (server-side)

GET https://api.boomtower.com/v1/organizations/3EXrgGUEVAI8xMFzT3j7IKo9ha5/members/consent?externalApp=partnerapp&externalId=YOUR_USER_ID
X-API-Key: YOUR_ORG_API_KEY

Deployment (iframe allowlist only)

This section applies to iframe embeds only. Popups load the consent page as a top-level document and do not require per-partner CSP changes on BoomTower.

The BoomTower web app sends Content-Security-Policy: frame-ancestors … on /embed/*. If a partner site is not listed, the browser blocks the iframe with a CSP error (you will see the consent URL in the console, but the frame stays blank).

Built-in defaults include headright.life, Lovable preview hosts, and 'self'. To allow additional partner domains in production, set on the BoomTower web deployment (Vercel):

EMBED_FRAME_ANCESTORS='self' https://partner.example.com https://*.partner.example.com

Important: when EMBED_FRAME_ANCESTORS is set in Vercel, it replaces the built-in list entirely. Include every origin you still need (existing partners + new ones).

After changing this variable, redeploy the BoomTower web app so the new header is served from www.boomtower.com/embed/consent.

Security notes

  • Never put X-API-Key in frontend code or the iframe URL.
  • Embed tokens are short-lived and should be treated like secrets (HTTPS only).
  • Pass allowedOrigin when creating embed sessions in production.
  • Validate event.origin against all BoomTower web hosts you use — not just one.
  • Add a server-side consent status check when the popup closes so linking still works if postMessage is missed.