Embedded Checkout
Embedded checkout puts Firmly’s checkout UI inside the destination’s surface as an iframe. The destination owns the surrounding chrome, branding, and conversation context; Firmly handles the secure parts — addresses, payment, order submission — inside the iframe. The two communicate via window.postMessage.
When to use this pattern
- The destination has a screen and wants checkout to feel native to it
- The destination wants a reduced PCI scope without going fully headless (payment never touches destination code)
- The destination wants Firmly’s checkout UX (saved cards, address validation, Click to Pay, PayPal) without rebuilding it
Don’t use this for conversational, voice, or fully autonomous surfaces — there’s no screen to embed into. Use Hosted Checkout (redirect) or Deep Link / Headless (no UI) instead.
The iframe URL
Point the iframe at the Firmly dropin URL — provided by Firmly — with the parameters below.
Required parameters
| Parameter | What it is |
|---|---|
_appId |
The destination’s App ID (UUID) |
domain |
The merchant domain — e.g. staging.luma.gift |
Optional parameters
| Parameter | Default | Purpose |
|---|---|---|
url |
— | Product URL to load directly into a product-detail view |
variant_id |
— | Skip product selection and pre-fill the variant |
quantity |
1 |
Initial quantity |
ui_mode |
fullscreen |
Layout mode: fullscreen, minimal-pdp, full-pdp |
force_pdp |
false |
Force the product-detail view to render |
skip_pdp |
false |
Skip product detail and jump straight to checkout |
flush_cart |
true |
Clear any existing cart before adding the item |
affiliate_url |
— | Affiliate-attribution URL for the order |
custom_properties |
— | JSON-encoded object of merchant-specific properties |
Example URL
<your-firmly-dropin-url>/buy?_appId=YOUR_APP_ID&domain=staging.luma.gift&url=https://staging.luma.gift/products/gift-card-25&ui_mode=fullscreen
Minimal HTML embed
FIRMLY_DROPIN_URL and FIRMLY_DROPIN_ORIGIN in the sample below are provided by Firmly.
<!DOCTYPE html><html><body><iframeid="firmly-checkout"src="FIRMLY_DROPIN_URL/buy?_appId=YOUR_APP_ID&domain=staging.luma.gift"style="width: 100%; height: 100vh; border: 0;"allow="payment; clipboard-read; clipboard-write"></iframe><script>window.addEventListener('message', (event) => {// SECURITY: only accept messages from the Firmly dropin origin.if (event.origin !== FIRMLY_DROPIN_ORIGIN) return;// See "postMessage events" below for the full catalogif (!event.data || typeof event.data !== 'object') return;const { action } = event.data;if (action === 'firmly::OrderPlaced') {console.log('Order placed:', event.data.session);// Navigate the user to your own confirmation, fire analytics, etc.}if (action === 'firmly::CheckoutClosed') {document.getElementById('firmly-checkout').style.display = 'none';}if (action === 'firmly::Error') {console.error('Firmly error:', event.data.error, event.data.context);}});</script></body></html>
Communication diagram
postMessage events
The iframe and the destination page communicate using window.postMessage. All Firmly messages are JSON with an action field as the discriminator.
Events from Firmly → destination page
action |
Payload fields | When it fires |
|---|---|---|
firmly::CheckoutClosed |
domain |
User closed the checkout view (back button, X, or tap-out) |
firmly::OrderPlaced |
url, session |
Order was successfully placed |
firmly::QuantityUpdated |
totalQuantity |
Cart item count changed |
firmly::CartUpdated |
cart |
Cart contents changed — full cart object included |
firmly::Error |
error, context |
An error occurred (validation, payment, network, etc.) |
Events from destination page → Firmly
action |
Payload fields | Purpose |
|---|---|---|
firmly::addToCart |
store_id, custom_properties, transfer |
Add a product to the cart without user interaction |
firmly::adjustSize |
data: { height } |
Tell the iframe to resize itself (rarely needed — the iframe self-sizes) |
Listening pattern (full example)
FIRMLY_DROPIN_ORIGIN below is the dropin origin that Firmly provides. Always use it as the target origin when posting into the iframe — never '*', which can leak payload data if the frame navigates.
const iframe = document.getElementById('firmly-checkout');window.addEventListener('message', (event) => {// SECURITY: verify the origin matches the Firmly dropin origin.if (event.origin !== FIRMLY_DROPIN_ORIGIN) return;switch (event.data?.action) {case 'firmly::OrderPlaced': {const { session } = event.data;// Destination's own confirmation, analytics, conversation handoff.window.parent.postMessage({ action: 'app::firmly-order', cartId: session?.cart_id, statusUrl: session?.urls?.order_status_page },'*');break;}case 'firmly::CartUpdated':updateCartBadge(event.data.cart);break;case 'firmly::CheckoutClosed':iframe.style.display = 'none';break;case 'firmly::Error':reportError(event.data.error, event.data.context);break;}});// Send a command into the iframe (e.g. after product disambiguation).// Target the Firmly dropin origin explicitly — never '*'.iframe.contentWindow.postMessage({action: 'firmly::addToCart',store_id: 'staging.luma.gift',custom_properties: { ref: 'chat-session-abc' },transfer: false},FIRMLY_DROPIN_ORIGIN);
Theming
The iframe inherits the merchant’s brand by default (logo, color, header style as configured in Firmly). For destination-level overrides, pass custom_properties as a URL-encoded JSON blob — Firmly will tell you which keys are configurable for the destination’s account.
Security
Two rules:
- Validate
event.originin the message listener. Only accept messages from the dropin origin that Firmly provided, and when posting into the iframe, always target that origin — never'*'. - Never include card data or credentials in
postMessagepayloads. The iframe is the trust boundary — the destination page never sees the card.
Versioning
Two versions of the embed exist in production:
- v4 — the generation the URL pattern, parameters, and events documented above apply to.
- v5 — a newer rewrite. URL is the same
/buypath; some routes accept an optional language prefix (e.g./pt-br/buy).
For a new integration, Firmly will confirm which version to target for your account — start there rather than assuming a default.
SDK shortcut
Firmly also publishes a small JavaScript SDK that wraps the iframe and event handling. FIRMLY_DROPIN_URL in the snippet below is provided by Firmly:
<script src="FIRMLY_DROPIN_URL/sdk/js"></script><script>window.firmly.buyNow({productUrl: 'https://staging.luma.gift/products/gift-card-25'});</script>
The SDK exposes window.firmly with three documented methods:
init(config)— one-time setup (App ID, merchant domain, dropin URL) before the firstbuyNowcall; Firmly provides the exact config shape for your accountbuyNow(...)— launch the dropin (acceptsproductUrl,affiliateUrl,layout)on(event, handler)— subscribe to events (e.g.orderPlaced)
Use the SDK for a one-liner; use the raw iframe for full control of the surrounding UX.