Checkout extensions render as iframes inside the customer-facing React checkout (/checkout). They use the same App Bridge wire format as admin extensions, but the host on /checkout exposes a customer-safe subset of actions — no resource pickers, no admin modals, no title bars.Use this page when building any extension whose target starts with checkout-*, checkout.*, or purchase.checkout.*.
Calls to unsupported actions return no response — the SDK call will reject with a timeout after 10 seconds. Design your extension to gracefully degrade if a non-checkout action fails.
If your extension iframe loads from a URL the merchant configured in app.json, the host will not pass apiKey or host query parameters by default. You can use the lightweight client below, or initialize the SDK manually with the parent origin:
let nextId = 0;const pending = new Map();window.addEventListener('message', (ev) => { const d = ev.data; if (!d || d.type !== 'APP_BRIDGE_RESPONSE') return; const cb = pending.get(d.id); if (cb) { pending.delete(d.id); cb(d.payload, d.error); }});function dispatchAndWait(action, payload, timeoutMs = 3000) { return new Promise((resolve, reject) => { const id = `ce_${++nextId}_${Date.now()}`; const to = setTimeout(() => { pending.delete(id); reject(new Error(`${action} timed out`)); }, timeoutMs); pending.set(id, (p, err) => { clearTimeout(to); err ? reject(new Error(err)) : resolve(p); }); window.parent.postMessage( { type: 'APP_BRIDGE_ACTION', action, payload: payload || {}, id }, '*' ); });}
const cart = await dispatchAndWait('CART_GET');// {// cartId: 'c1-a1b2c3d4-...', // value of the `cart` cookie// items: [{ id, productId, variantId, title, quantity, price }],// itemCount: 3,// currency: 'INR',// note: '',// }
The cartId field exposes the value of the cart cookie so your extension can hit /api/cart/* endpoints directly without re-reading cookies. The cookie itself is non-HttpOnly (document.cookie works too), but going through the bridge keeps your code portable.
If the customer hasn’t typed an email yet, email is an empty string. The customer is not necessarily logged in; do not assume this maps to a Customers row.
Returns the live cost breakdown as standard { amount, currencyCode } money objects (suitable for currency-formatted display). For the flat scalar breakdown used by the order summary, see CHECKOUT_TOTALS_GET.
The minimal identity the checkout has collected so far. Useful for personalising extension copy without prompting the customer.
const identity = await dispatchAndWait('BUYER_IDENTITY_GET');// {// email: 'alex@example.com', // '' if the field is empty// phone: '+91-9876543210', // from selected/manual address// customer: { id: 'cus-uuid' } // null when not logged in// }
No PII beyond what the customer has already entered into the host.
storeDomain is the store’s canonical LaunchMyStore domain. Custom domains aren’t reflected here — use storefrontUrl for the public-facing URL the customer is on.
Capability flags describing what the current checkout host allows your extension to mutate. Use this to feature-gate your UI instead of guessing or hard-coding.
canRemoveDiscountCodes / giftCards.canUpdate / metafields.canUpdate are false today — calling the matching mutation actions will return a deterministic “not supported” error so you don’t hang on a timeout.
Write a delivery note / order instruction. Persisted to the cart’s note field and saved on the order when it’s placed.
await dispatchAndWait('ORDER_NOTE_SET', { note: 'Leave with the concierge, please do not ring the bell.',});// { ok: true, note: 'Leave with the concierge, ...' }
Field
Type
Required
Description
note
string
Yes
Truncated to 500 chars
Pass an empty string to clear the note. This dispatches setCartNote on the Redux cart slice; redux-persist will store it, and the order placement flow will include it in addClientOrder.payload.note.
The host fills its coupon input field with code. The customer still confirms by clicking Apply in the native UI — your extension does not bypass merchant coupon rules.
The checkout host exposes a declarative cart-mutation surface: an
extension calls one of the actions below, the host applies the mutation
to Redux, re-runs verifyCart, and responds with the updated cart
envelope. Mutations follow a stable, action-builder-style call shape so
extensions stay portable across hosts.Use the Cart class from @launchmystore/app-bridge for type safety; the
raw action names are documented below for reference.
import { Cart } from '@launchmystore/app-bridge';const cart = Cart.create(app);
Internally the host dispatches Redux actions addToCart,
updateQuantity, or removeFromCart and re-fetches the cart envelope.
When addCartLine targets a variant already in the order summary, the
new line is enriched from existing cart / verifyCart data so name, image,
and price aren’t lost — important for post-purchase upsells where the
line is added from a different page.
removeDiscountCode currently returns error: 'not supported yet'
code
string
yes
Trimmed to 64 chars
Adding a code fills the host’s coupon input and runs the merchant’s
existing applyCoupon flow — eligibility, expiry, and min-cart checks
all apply. The host responds with { ok: true, code } once the apply
attempt completes.
Gift cards aren’t fully wired yet. Calling this action returns
{ ok: false, applicable: false } so apps can detect unsupported state
without hanging on a 10-second timeout. The action name is reserved
for forward compatibility — when gift cards land, the contract will
follow the standard applyGiftCardChange shape.
Update the customer’s shipping address. Only updateShippingAddress is supported today — there is no add/remove distinction because checkout holds exactly one address at a time.
The host merges incoming fields with the existing address (only non-empty values overwrite). After the write, verifyCart re-runs so shipping options and totals refresh on the next read.Error responses:
'address required' — no address object passed
'address mutation not available' — the host page hasn’t wired the writer (e.g. read-only screens)
'unknown address change type' — type is not updateShippingAddress
Block the customer from advancing to payment until your condition is met
— a callback-based interceptor that runs every time the buyer clicks
Place order.
import { BuyerJourney } from '@launchmystore/app-bridge';const journey = BuyerJourney.create(app);const unsubscribe = journey.intercept(() => { if (!termsAcceptedCheckbox.checked) { return { behavior: 'block', reason: 'You must accept the terms before continuing.', }; } return { behavior: 'allow' };});// Stop interceptingunsubscribe();// orjourney.destroy();
The host dispatches BUYER_JOURNEY_INTERCEPT_REQUEST with a
requestId whenever the buyer clicks Place order; the SDK runs your
callback and responds via BUYER_JOURNEY_INTERCEPT_RESPONSE with the
same requestId. The callback can be async — the host waits for the
response before proceeding.Result shape:
Wire format below matches what you ship in your app’s extensions.checkoutExtensions[] array. Targets prefixed with checkout- are the canonical LaunchMyStore slot names; targets prefixed with purchase.checkout.* are the dot-style alias accepted for ecosystem portability.
Two important things to understand about what survives into the placed order:
ORDER_NOTE_SET survives. The note is on the cart slice (state.cart.note), included in addClientOrder.payload.note, and saved on the Orders.note column.
COUPON_APPLY_REQUEST is best-effort. It fills the host’s coupon input — the customer still has to click Apply, and the merchant’s coupon rules (eligibility, expiry, min-cart) still apply. There is no way for an extension to forcibly apply a coupon that the host would reject.
TOAST_SHOW is ephemeral. Toasts vanish on page reload.
Function-based price changes (cart_transform, discount, shipping_rate, payment_customization, delivery_customization, order_validation) are separate from App Bridge — they’re declarative WASM functions that run on verifyCart and addClientOrder. Use those for guaranteed cart mutations; use App Bridge actions for opt-in customer interaction.
The checkout host listens for window.message events with type: 'APP_BRIDGE_ACTION'. Origin is not currently checked because extension iframes are same-origin under /extensions/.... Do not rely on the message channel for authentication — the host treats every same-origin frame as trusted.
Customer PII (full address, phone, payment details) is not exposed via App Bridge. Only the email the customer has already typed is available via CUSTOMER_GET. If you need more, your extension must collect it directly from the customer.
Iframes use sandbox="allow-scripts allow-forms allow-popups allow-same-origin". Top-level navigation is blocked; popups open in a new tab.
My dispatchAndWait always times out.
The host probably isn’t mounted yet. The broker is registered inside <Checkout>, so it’s not listening until React mounts. Wait for DOMContentLoaded plus a frame before dispatching, or retry once on timeout.COUPON_APPLY_REQUEST returns ok: true but nothing happens.
The host puts the code into the input box but does not automatically click Apply — that’s by design so merchant coupon rules run normally. If your extension wants to provide a smoother flow, hold the code yourself and surface a clear “click Apply to use this code” affordance.Iframe height won’t change past 800px.
The host clamps APP_BRIDGE_RESIZE to [60, 800]. Split content across multiple slots or use internal scrolling if you need more vertical space.My iframe shows but I never get any responses.
Check the iframe’s sandbox attribute. The host requires allow-scripts and allow-same-origin; otherwise postMessage and window.parent access are blocked. The default <CheckoutExtensionSlot> already passes the correct sandbox flags.