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

Hosted Checkout

Hosted checkout is the pattern where Firmly hosts the entire checkout UI. The destination prepares the cart (or just the intent), generates a Firmly checkout URL, and sends the user to it. Firmly renders the entire checkout — addresses, payment, review, confirm. After the order is placed, the user lands on the merchant’s order-confirmation page.

“Hosted” means Firmly hosts the page and owns the UX. Both the infrastructure and the design are Firmly’s. The destination owns the conversation around it.

​​ When to use it

  • The destination wants Firmly’s checkout UX rather than building its own
  • Voice or conversational surfaces that hand off to a phone screen for the final step
  • Compliance-conscious destinations that want PCI scope entirely on Firmly’s side

Don’t use this for surfaces with no human at a screen (use Deep Link / Headless), or when the surrounding UX must match the destination’s brand (use Embedded Checkout — same URL, used as iframe src).

​​ The checkout URL

The destination redirects the user to the Firmly dropin URL with the parameters below. The dropin URL is provided by Firmly — it is the same URL used for Embedded Checkout, just used as a top-level navigation target instead of an iframe src.

​​ Required parameters

Parameter What it is
_appId Your App ID (UUID)
domain The merchant domain (e.g. staging.luma.gift)

​​ Useful optional parameters

Parameter Default Purpose
url — Product URL to pre-load into a PDP view
variant_id — Pre-select a variant
quantity 1 Pre-set quantity
ui_mode fullscreen Layout: fullscreen, minimal-pdp, full-pdp
skip_pdp false Skip product detail, jump straight to checkout
force_pdp false Force the product detail view
flush_cart true Clear any existing cart before adding the item
affiliate_url — Affiliate attribution URL
custom_properties — JSON-encoded 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
&skip_pdp=true

​​ The handoff pattern

​​ Agent assembles intent

The destination resolves the user’s request to a product (via Catalog Search or Get Product), then constructs the checkout URL with the appropriate parameters above.

​​ Agent issues the URL

Depending on the surface:

  • Chat — send the link inline
  • Voice — text the URL via SMS or push it to a connected screen
  • Web app — window.location.href = checkoutUrl or open in a new tab
  • Mobile — open in an in-app browser or system browser

​​ User completes checkout on Firmly’s page

Firmly handles product detail, address, shipping, payment, review, confirm. The destination is not involved during this window.

​​ After the order: user lands on the merchant’s thank-you page

The dropin redirects the user to data.urls.thank_you_page from the complete-order response — that’s the merchant’s confirmation page (e.g. <merchant>.com/.../account/orders/...). Today the user does not return to a destination-controlled URL automatically.

​​ Agent re-engages on next user contact

See the next section for how the destination knows the order was placed.

​​ How the destination learns the order was placed

This is the most important section of this page. Read it before scoping the integration.

There is no return_url parameter that redirects the user back to your domain with order details. That option doesn’t exist today. Here are the three patterns that actually work:

If your surface can render an iframe, embed the dropin URL in a full-screen iframe and listen for firmly::OrderPlaced. This is technically Embedded Checkout — the only difference from “hosted” is the iframe takes the full viewport. You get the order details via postMessage immediately.

FIRMLY_DROPIN_ORIGIN in the sample below is the dropin origin that Firmly provides.


window.addEventListener('message', (event) => {
if (event.origin !== FIRMLY_DROPIN_ORIGIN) return;
if (event.data?.action === 'firmly::OrderPlaced') {
const { url, session } = event.data;
// url = merchant's thank-you page URL
// session = full cart object including cart_id, custom_properties, urls
handleOrderPlaced(session);
}
});

See Embedded Checkout — postMessage events for the full event catalog.

​​ Pattern B — Polling cart status (for non-iframe surfaces)

If you can’t iframe (voice, SMS handoff, AMP, certain in-app browsers), you need to detect order placement by polling the cart endpoint until its cart_status transitions to submitted. This requires you to correlate the checkout with a session you control. Bootstrap a browser session server-side before sending the user to checkout, hold onto its access_token, and poll the cart with that same token — the cart the user acts on in the dropin is the one bound to your session. Pass the token into the poller (not a bare device_id, which is not a checkout-URL parameter):


// Before sending the user to checkout, you bootstrapped a browser session server-side
// and kept its access_token. Poll the cart with that token until it becomes submitted.
async function pollForOrder(accessToken, domain) {
for (let i = 0; i < 60; i++) { // 60 attempts = ~10 minutes at 10s interval
const cart = await fetch(
`https://api.firmly.work/api/v2/domains/${domain}/cart`,
{ headers: { 'x-firmly-authorization': accessToken } }
).then(r => r.json());
if (cart.cart_status === 'submitted') {
return cart; // contains cart_id and custom_properties
}
await new Promise(r => setTimeout(r, 10000));
}
throw new Error('Order not detected within timeout');
}

​​ Pattern C — Out-of-band confirmation

For voice agents that don’t have a screen at all, the simplest pattern is: text the user the checkout URL, accept that the destination loses real-time visibility, and re-engage when the user mentions the order in the next conversation turn. Combine with Pattern B (polling) if you want proactive notification.

​​ Sample agent flow


Agent: "I'll send you a link to complete the purchase."
[Agent constructs URL: <your-firmly-dropin-url>/buy?_appId=...&domain=...]
[Agent texts URL via SMS, OR redirects user via window.location, OR opens iframe]
[User completes checkout on Firmly's page]
Option A (iframe): Agent immediately receives firmly::OrderPlaced postMessage
Option B (no iframe): Agent polls cart status every 10s; sees cart_status: "submitted"
Option C (voice + out-of-band): Agent waits for user to come back to the conversation
Agent: "Great — your order is placed."

​​ Theming

The hosted page inherits the merchant’s brand by default. Destination-level overrides (logo, accent color, copy) are configured per App ID — talk to Firmly. There’s no per-call theming via URL parameters at this layer.

​​ What you don’t have to build

When you use hosted checkout, you skip:

  • Address entry forms and validation
  • Payment field rendering and PCI compliance
  • Shipping method selection UI
  • Order review and confirm screens
  • Refund/dispute UI (handled by merchant)

This is why hosted is the fastest first integration — you’re not building any of the above. The trade-off is the user ends up on the merchant’s thank-you page, not yours, and you need one of the three patterns above to detect order completion programmatically.