Docs
Firmly Agentic Commerce
Set theme to dark (⇧+D)

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>
<iframe
id="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 catalog
if (!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:

  1. Validate event.origin in the message listener. Only accept messages from the dropin origin that Firmly provided, and when posting into the iframe, always target that origin — never '*'.
  2. Never include card data or credentials in postMessage payloads. 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 /buy path; 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 first buyNow call; Firmly provides the exact config shape for your account
  • buyNow(...) — launch the dropin (accepts productUrl, 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.