Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt

Use this file to discover all available pages before exploring further.

App Bridge for Checkout

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.*.

What’s different from admin App Bridge

CapabilityAdmin host (TeamInfra)Checkout host (CustomerLMS)Post-purchase host (CustomerLMS)
Wire formatAPP_BRIDGE_ACTION / APP_BRIDGE_RESPONSESameSame
SDK package@launchmystore/app-bridgeSameSame
TOAST_SHOW
BRIDGE_PING✓ (host: 'checkout')✓ (host: 'post-purchase')
CART_GET / CHECKOUT_TOTALS_GET
CART_LINES_CHANGE✓ (add / update / remove)✓ (add only)
DISCOUNT_CODE_CHANGE✓ (add)
NOTE_CHANGE, ATTRIBUTE_CHANGE
GIFT_CARD_CHANGE✓ (stub — returns applicable: false)
COST_GET, BUYER_IDENTITY_GET, LOCALIZATION_GET
SHIPPING_ADDRESS_GET, BILLING_ADDRESS_GET
DELIVERY_GROUPS_GET, SHIPPING_ADDRESS_CHANGE
DISCOUNT_CODES_GET, APPLIED_GIFT_CARDS_GET
SHOP_GET, INSTRUCTIONS_GET, ATTRIBUTES_GET
METAFIELD_CHANGE✓ (stub — returns error)
ORDER_NOTE_SET / COUPON_APPLY_REQUEST (legacy)
ORDER_GET
CUSTOMER_GET / CURRENCY_GET
CLIPBOARD_WRITE
REDIRECT
DONE (goes to /orders/<id>)
BUYER_JOURNEY_INTERCEPT_REQUEST/RESPONSE
RESOURCE_PICKER_OPEN✗ (merchant-only)
TITLE_BAR_UPDATE, NAV_MENU_UPDATE✗ (no admin chrome)
MODAL_OPEN, CONTEXTUAL_SAVE_BAR✗ (would block checkout)
SESSION_TOKEN_REQUEST✗ (no admin session at checkout)
SCANNER_OPEN, FULLSCREEN_*
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.

Initializing

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 },
      '*'
    );
  });
}

Action Reference

BRIDGE_PING

Capability handshake. Resolves with { ok: true, host: 'checkout' } if the checkout host is listening.
const reply = await dispatchAndWait('BRIDGE_PING');
// → { ok: true, host: 'checkout' }
Use this to detect whether your extension is running inside the checkout host or some other surface, and feature-flag accordingly.

TOAST_SHOW

Show a toast notification rendered by the checkout’s toast system.
await dispatchAndWait('TOAST_SHOW', {
  message: 'Gift wrap added',
  variant: 'success',  // or 'error'
});
FieldTypeRequiredDescription
messagestringYesUp to 200 chars (longer messages are truncated)
variant'success' | 'error'NoDefault success
Response: { ok: true }

CART_GET

Read the current cart contents.
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.

CHECKOUT_TOTALS_GET

Read the live price breakdown from verifyCart. This is the same data the host renders in the order summary, so values stay consistent.
const totals = await dispatchAndWait('CHECKOUT_TOTALS_GET');
// {
//   subtotal:          9600.00,
//   bundleDiscount:    23800.00,   // from cart_transform functions
//   appDiscount:          63.60,   // from discount functions (line/order)
//   shippingDiscount:     50.00,   // from discount functions (shipping)
//   couponDiscount:      225.00,   // from merchant coupon
//   shipping:            100.00,
//   tax:                  18.00,
//   finalPrice:        10273.52,
//   currency: 'INR',
// }
Refresh by polling — there is no push notification today. A 2-3 second interval is reasonable.

CUSTOMER_GET

Read the email the customer has typed at checkout. No PII beyond what the customer has already entered into the host.
const { email } = await dispatchAndWait('CUSTOMER_GET');
// { email: 'alex@example.com' }
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.

CURRENCY_GET

const { currency } = await dispatchAndWait('CURRENCY_GET');
// { currency: 'INR' }
Identical to CART_GET().currency — exposed separately so simple extensions don’t have to fetch the whole cart.

COST_GET

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.
const cost = await dispatchAndWait('COST_GET');
// {
//   subtotalAmount:       { amount: 9600.00,  currencyCode: 'INR' },
//   totalDiscountAmount:  { amount:  288.60,  currencyCode: 'INR' },  // cart_transform + appDiscount + couponDiscount
//   totalShippingAmount:  { amount:  100.00,  currencyCode: 'INR' },
//   totalTaxAmount:       { amount:   18.00,  currencyCode: 'INR' },
//   totalAmount:          { amount: 9429.40,  currencyCode: 'INR' },
// }
Like CHECKOUT_TOTALS_GET, this is a snapshot — there is no push notification. Re-poll after any cart / discount / address change to stay in sync.

BUYER_IDENTITY_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.

LOCALIZATION_GET

Country, language, and currency picked by the customer (or inherited from the store’s auto-detected locale).
const loc = await dispatchAndWait('LOCALIZATION_GET');
// {
//   country:  { isoCode: 'IN',  name: 'IN' },
//   language: { isoCode: 'en' },
//   currency: { isoCode: 'INR', name: 'INR' },
// }
Use this to switch between localised marketing copy or hide an extension entirely in unsupported regions.

SHIPPING_ADDRESS_GET

The customer’s selected delivery address. Returns address: null until they pick / type one.
const { address } = await dispatchAndWait('SHIPPING_ADDRESS_GET');
// address = {
//   firstName:    'Alex',
//   lastName:     'Patel',
//   address1:     '12 MG Road',
//   address2:     'Apt 4B',
//   city:         'Mumbai',
//   provinceCode: 'MH',
//   province:     'MH',
//   countryCode:  'IN',
//   country:      'IN',
//   zip:          '400001',
//   phone:        '+91-9876543210',
//   company:      '',
// } | null

BILLING_ADDRESS_GET

LaunchMyStore checkout does not split shipping/billing — this returns the same address payload as SHIPPING_ADDRESS_GET.
const { address } = await dispatchAndWait('BILLING_ADDRESS_GET');

DISCOUNT_CODES_GET

Coupon codes the customer has applied to this cart.
const { discountCodes } = await dispatchAndWait('DISCOUNT_CODES_GET');
// discountCodes = [{ code: 'WELCOME10', applicable: true }]   // [] when no code is applied

APPLIED_GIFT_CARDS_GET

Gift cards aren’t fully wired yet — always returns an empty array. Apps can call this safely to detect a gift-card-free state without hanging.
const { appliedGiftCards } = await dispatchAndWait('APPLIED_GIFT_CARDS_GET');
// appliedGiftCards = []

DELIVERY_GROUPS_GET

Available shipping options for the current cart and address. Mirrors what the order summary renders next to “Shipping”.
const { deliveryGroups } = await dispatchAndWait('DELIVERY_GROUPS_GET');
// deliveryGroups = [
//   {
//     id: 'default',
//     groupType: 'oneTimePurchase',
//     deliveryOptions: [
//       {
//         id: 'shipping-zone-uuid',
//         title: 'Standard Shipping',
//         cost: { amount: 100.00, currencyCode: 'INR' },
//         description: 'Delivery in 3-5 business days',
//       },
//       // ...
//     ],
//     selectedDeliveryOptionId: null,    // host doesn't track per-group selection today
//   },
// ]
A single delivery group is returned today; future multi-shipment support will expand this into one group per shipment.

SHOP_GET

Identity of the store the checkout is running on.
const shop = await dispatchAndWait('SHOP_GET');
// {
//   id:            'store-uuid',
//   name:          'Acme Store',
//   storeDomain:   'acme.launchmystore.io',     // canonical store domain
//   storefrontUrl: 'https://acme.launchmystore.io',
// }
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.

INSTRUCTIONS_GET

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.
const ins = await dispatchAndWait('INSTRUCTIONS_GET');
// {
//   attributes: { canUpdateAttributes: true },
//   delivery:   { canSelectCustomAddress: true },
//   discounts:  { canUpdateDiscountCodes: true, canRemoveDiscountCodes: false },
//   notes:      { canUpdateNote: true },
//   cartLines:  { canUpdateLines: true, canAddLines: true, canRemoveLines: true },
//   giftCards:  { canUpdate: false },
//   metafields: { canUpdate: false },
// }
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.

ATTRIBUTES_GET

All cart-level attributes apps and other extensions have written.
const { attributes } = await dispatchAndWait('ATTRIBUTES_GET');
// attributes = { gift_wrap: 'true', delivery_window: 'evening' }
The same map is persisted onto the order at placement and queryable in Liquid via order.attributes.

ORDER_NOTE_SET

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, ...' }
FieldTypeRequiredDescription
notestringYesTruncated 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.

COUPON_APPLY_REQUEST

Forward a coupon code to the host’s coupon input.
await dispatchAndWait('COUPON_APPLY_REQUEST', { code: 'WELCOME10' });
// { ok: true, code: 'WELCOME10' }
FieldTypeRequiredDescription
codestringYesTruncated to 64 chars
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.

Cart Mutation Actions

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);

CART_LINES_CHANGE

Add, update, or remove cart lines. Accepts a single change or an array.
// Add a new line
await cart.applyCartLinesChange({
  type: 'addCartLine',
  merchandiseId: 'gid://customerlms/Variant/abc-123',
  quantity: 1,
  attributes: [{ key: 'gift_wrap', value: 'true' }],
});

// Update a line's quantity
await cart.applyCartLinesChange({
  type: 'updateCartLine',
  id: 'gid://customerlms/CartLine/line-uuid',
  quantity: 3,
});

// Remove a line
await cart.applyCartLinesChange({
  type: 'removeCartLine',
  id: 'gid://customerlms/CartLine/line-uuid',
});

// Batch
await cart.applyCartLinesChange([
  { type: 'addCartLine', merchandiseId: 'gid://...', quantity: 1 },
  { type: 'removeCartLine', id: 'gid://...' },
]);
FieldTypeRequiredNotes
type'addCartLine' | 'updateCartLine' | 'removeCartLine'yes
merchandiseIdstring (gid)for addCartLinegid://customerlms/Variant/<uuid>
idstring (gid)for update/removegid://customerlms/CartLine/<uuid>
quantityintegerfor add/updateUpdates with 0 remove the line
attributes{key,value}[]noPer-line properties — stored on the order line
Response:
{
  ok: true,
  results: Array<{ type: string, ok: boolean, error?: string }>,
}
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.

DISCOUNT_CODE_CHANGE

Apply or remove a discount code.
await cart.applyDiscountCodeChange({
  type: 'addDiscountCode',
  code: 'WELCOME10',
});
FieldTypeRequiredNotes
type'addDiscountCode' | 'removeDiscountCode'yesremoveDiscountCode currently returns error: 'not supported yet'
codestringyesTrimmed 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.

NOTE_CHANGE

Update or remove the cart note.
await cart.applyNoteChange({
  type: 'updateNote',
  note: 'Leave with concierge — apt 4B',
});

await cart.applyNoteChange({ type: 'removeNote' });
FieldTypeRequiredNotes
type'updateNote' | 'removeNote'yes
notestringfor updateNoteTruncated to 500 chars
The note flows through Redux (setCartNote) → addClientOrder.payload.note → persisted Order.note column.

ATTRIBUTE_CHANGE

Set or remove a cart-level attribute (key/value pair stored on the order).
await cart.applyAttributeChange({
  type: 'updateAttribute',
  key: 'gift_wrap',
  value: 'true',
});

await cart.applyAttributeChange({
  type: 'removeAttribute',
  key: 'gift_wrap',
});
FieldTypeRequiredNotes
type'updateAttribute' | 'removeAttribute'yes
keystringyesTruncated to 64 chars
valueanyfor updateJSON-serialisable
Attributes are stored on the cart slice and persisted with the order so they’re queryable in Liquid templates and admin order detail pages.

GIFT_CARD_CHANGE (stub)

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.
const res = await cart.applyGiftCardChange({
  type: 'addGiftCard',
  code: 'GC-12345',
});
// → { ok: false, applicable: false }

SHIPPING_ADDRESS_CHANGE

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.
await cart.applyShippingAddressChange({
  type: 'updateShippingAddress',
  address: {
    firstName: 'Alex',
    lastName:  'Patel',
    address1:  '12 MG Road',
    city:      'Mumbai',
    provinceCode: 'MH',
    countryCode:  'IN',
    zip:       '400001',
    phone:     '+91-9876543210',
  },
});
// → { ok: true, address: { ...normalised standard shape... } }
FieldTypeRequiredNotes
type'updateShippingAddress'yesOnly one type today
addressobjectyesField names match SHIPPING_ADDRESS_GET output
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

METAFIELD_CHANGE (stub)

Cart-level metafields aren’t stored yet — the backend metafields surface is owner-scoped (product / customer / order / etc.). Calling this action always returns:
{ payload: {}, error: 'cart metafields not supported yet' }
Reserved for forward compatibility. Use owner-scoped metafield REST endpoints (/metafields / /api/v1/metafields) for non-cart writes today.

Buyer Journey Intercept

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 intercepting
unsubscribe();
// or
journey.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:
FieldTypeDescription
behavior'allow' | 'block'Required
reasonstringShown to the customer when block
performfunctionRuns after allow — useful for side-effects

APP_BRIDGE_RESIZE

Tell the host to resize your iframe to fit content. Use a ResizeObserver to call this automatically.
function resize() {
  window.parent.postMessage(
    { type: 'APP_BRIDGE_RESIZE', height: document.documentElement.scrollHeight },
    '*'
  );
}
new ResizeObserver(resize).observe(document.body);
The host clamps the height to [60, 800] pixels. This is a fire-and-forget message — no response.

Available Targets

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.
TargetRendersCustomer-facing use cases
checkout-contact-afterAfter email/login rowDelivery notes, account-creation prompts, newsletter signups
checkout-shipping-afterAfter address formAddress validation, location-based offers
checkout-payment-beforeAbove payment methodsTrust badges, payment-method explainers
checkout-payment-afterBelow payment methods”Why you can trust us”, security seals
checkout-order-summary-beforeTop of order summaryCoupon helpers, promo banners
checkout-order-summary-afterBottom of order summaryLive savings calculators, loyalty point preview
purchase.checkout.block.renderCustom block placementGeneral-purpose surfaces
purchase.thank-you.block.renderThank-you page after order placedPost-purchase upsells, review prompts

Worked Example: Delivery Instructions Card

A complete iframe that asks the customer for delivery instructions and saves them via ORDER_NOTE_SET.
<!doctype html>
<html><body>
  <textarea id="note" placeholder="Leave a note for the courier"></textarea>
  <button id="save">Save instructions</button>
  <div id="status"></div>

  <script>
    let nextId = 0;
    const pending = new Map();
    window.addEventListener('message', (ev) => {
      const d = ev.data;
      if (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) {
      return new Promise((resolve, reject) => {
        const id = `dn_${++nextId}_${Date.now()}`;
        const to = setTimeout(() => reject(new Error('timeout')), 3000);
        pending.set(id, (p, err) => { clearTimeout(to); err ? reject(new Error(err)) : resolve(p); });
        parent.postMessage({ type: 'APP_BRIDGE_ACTION', action, payload, id }, '*');
      });
    }

    document.getElementById('save').addEventListener('click', async () => {
      const note = document.getElementById('note').value.trim();
      await dispatchAndWait('ORDER_NOTE_SET', { note });
      await dispatchAndWait('TOAST_SHOW', { message: 'Instructions saved', variant: 'success' });
      document.getElementById('status').textContent = 'Saved.';
    });

    new ResizeObserver(() => parent.postMessage(
      { type: 'APP_BRIDGE_RESIZE', height: document.documentElement.scrollHeight },
      '*'
    )).observe(document.body);
  </script>
</body></html>
Register the manifest in your app.json:
{
  "extensions": {
    "checkoutExtensions": [
      {
        "target": "checkout-contact-after",
        "appId": "delivery-instructions-1",
        "appName": "Delivery Instructions",
        "handle": "delivery-instructions",
        "iframeUrl": "/extensions/{your-app}/delivery-instructions.html"
      }
    ]
  }
}

Persistence Guarantees

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.

Security Notes

  • 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.

Troubleshooting

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.