Drop the LawBook consultation intake form into any webpage in under two minutes.
No framework required — a single <script> tag does the work.
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> 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.
| 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%. |
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.
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-resizeFired 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-successFired 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-errorFired 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
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);
}); 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.
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.
https://*.example.com) are not supported.
Each subdomain must be added individually.
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>
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.