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 = checkoutUrlor 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:
Pattern A — Iframe + postMessage (recommended for web surfaces)
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, urlshandleOrderPlaced(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 intervalconst 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 postMessageOption 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 conversationAgent: "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.