Embed Widget Integration Guide

Drop the LawBook consultation intake form into any webpage in under two minutes. No framework required — a single <script> tag does the work.

Quick-start

Add the container <div> and the loader script wherever you want the form to appear. The HMAC parameters (data-sig, data-ts, data-nonce) are generated server-side when you create a signed embed token from your tenant admin dashboard.

<!-- 1. Container — paste where the form should appear -->
<div
  data-tenant="YOUR_TENANT_ID"
  data-form="consultation-booking"
  data-embed="homepage-hero"
  data-sig="HMAC_SIGNATURE"
  data-ts="UNIX_TIMESTAMP"
  data-nonce="RANDOM_NONCE"
></div>

<!-- 2. Loader — place once, anywhere after the container -->
<script src="https://lawbook.work/embed/intake.js" defer></script>
HMAC token required. The data-sig / data-ts / data-nonce attributes must be present. Tokens expire in 10 minutes and are single-use (nonce replay is rejected). Generate a fresh signed token for each page load via your admin dashboard or the embed-token-sign operator action.

Attributes

Attribute Required Description
data-tenant required Your tenant ID (UUID or slug) — identifies which firm's form to render.
data-form required Form key. Currently only consultation-booking is supported.
data-embed optional Unique embed instance identifier. Use different values to place the form on multiple pages (e.g. homepage-hero, contact-page). Defaults to default.
data-sig required HMAC-SHA256 signature over tenant_id|embed_id|nonce|ts. Server-generated.
data-ts required Unix timestamp (seconds) at which the token was signed. 10-minute TTL.
data-nonce required Cryptographic nonce. Single-use — replayed nonces are rejected with 401.
data-height optional Initial iframe height in pixels before first resize event. Default: 480.
data-width optional Initial iframe width. Default: 100%.

postMessage events

The iframe emits structured messages to window.parent. The loader converts these into DOM CustomEvents dispatched on the container element with the prefix lawbook:. You can listen to either the raw window.message events or the convenience DOM events.

Raw message events are origin-guarded. The loader only processes messages where event.origin matches the Supabase functions domain and event.source is the widget iframe.

Form resize — form-resize

Fired whenever the form content height changes. The loader automatically adjusts the iframe height; listen if you need to react in your own layout.

// Raw window.message (type: 'form-resize')
window.addEventListener('message', function (event) {
  if (event.data?.type === 'form-resize') {
    console.log('new height:', event.data.height);
  }
});

// DOM CustomEvent on container
document.querySelector('[data-tenant]').addEventListener('lawbook:resize', function (e) {
  console.log('new height:', e.detail.height);
});

Submission success — submission-success

Fired once when a form is successfully submitted. The loader replaces the iframe with a success state; listen to fire analytics or redirect.

// Raw window.message (type: 'submission-success')
window.addEventListener('message', function (event) {
  if (event.data?.type === 'submission-success') {
    // e.g. gtag('event', 'consultation_request', { ... });
  }
});

// DOM CustomEvent on container
document.querySelector('[data-tenant]').addEventListener('lawbook:success', function () {
  window.location.href = '/thank-you';
});

Submission error — submission-error

Fired when the server rejects the submission. The iframe remains mounted for retry.

// Raw window.message (type: 'submission-error', error: string)
window.addEventListener('message', function (event) {
  if (event.data?.type === 'submission-error') {
    console.error('submission error:', event.data.error);
  }
});

// DOM CustomEvent on container
document.querySelector('[data-tenant]').addEventListener('lawbook:error', function (e) {
  console.error('error code:', e.detail.error);
});

Validation error — validation-error

Fired when client-side validation fails before the submit request is made. detail.fields is an array of the failing field names.

// DOM CustomEvent on container
document.querySelector('[data-tenant]').addEventListener('lawbook:validation-error', function (e) {
  console.warn('invalid fields:', e.detail.fields);
});

Security model

HMAC-signed URLs with 10-minute TTL. Each embed token is an HMAC-SHA256 signature over tenant_id|embed_id|nonce|ts using a Vault-stored secret. Tokens expire after 10 minutes and nonces are tracked in a seen-set — replayed tokens are rejected with HTTP 401.

frame-ancestors enforcement. The embed-intake-render edge function reads tenant_embed_origins from the database and sets Content-Security-Policy: frame-ancestors <origin1> <origin2> ... on the iframe response. Partner sites that are not on the allowlist cannot frame the form — the browser refuses to render it.

Sandboxed iframe. The loader mounts the iframe with sandbox="allow-forms allow-scripts allow-same-origin". This permits form submission and the postMessage channel while blocking top-level navigation, pop-ups, and plugin content.

BotID (Vercel). The submission edge endpoint is protected by Vercel BotID at friction level challenge. Bot-detected requests are rejected with HTTP 403 before any database activity. BotID failures do not consume the tenant's rate-limit token bucket.

Per-tenant rate limits. The embed_intake surface has per-tenant token-bucket rate limiting. Defaults by plan: Solo 10 req/min, Firm 50 req/min, Enterprise 200 req/min.

Allowlisting your domain

Before the embed will render on your site you must add your origin to the tenant allowlist. An origin is scheme://host[:port] with no trailing slash, e.g. https://smolinseklaw.com or http://localhost:3000.

To add or remove origins, go to your tenant admin at /admin/embed-origins. Changes take effect immediately — no redeploy required.

Wildcard origins (e.g. https://*.example.com) are not supported. Each subdomain must be added individually.

Global API

The loader exposes a minimal API on window.LawBookEmbed. No other globals are set.

Property Type Description
LawBookEmbed.version string Loader version string, e.g. "1.0.0".
LawBookEmbed.init() function Scans the document for uninitialized embed containers and mounts each. Called automatically on DOMContentLoaded unless the script tag carries data-lawbook-no-auto-init.
LawBookEmbed.mount(el) function Mount a specific container element. Use when you inject containers dynamically after page load.
<!-- Disable auto-init and mount manually -->
<script src="https://lawbook.work/embed/intake.js"
        data-lawbook-no-auto-init defer></script>
<script>
  document.addEventListener('DOMContentLoaded', function () {
    var el = document.getElementById('my-container');
    window.LawBookEmbed.mount(el);
  });
</script>

Fallback behaviour

If the iframe fails to load within 5 seconds (e.g. blocked by the parent page's own CSP or a content blocker), the loader replaces the container with a plain "Open booking form" link pointing to https://<tenant-slug>.lawbook.work/intake/<form>?embed=<id>. This ensures prospective clients can always reach the form.