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.

Post-Purchase Bridge

Post-purchase extensions render between order-placement and the order-confirmation page. They run as same-origin iframes inside CustomerLMS at the /checkout/post-purchase route. The host implements a strict subset of the full App Bridge action set — anything that would interfere with order finalisation (modals, contextual save bars, admin chrome) is intentionally absent. This page is the action-by-action reference for that subset. For end- to-end concepts (manifest shape, sandbox flags, mount lifecycle) see Post-Purchase Extensions.

Initializing

Post-purchase iframes are same-origin, so createApp({ host: 'post-purchase' }) is the canonical setup — the SDK detects the sentinel and uses window.location.origin as the host origin.
import { createApp } from '@launchmystore/app-bridge';

const app = createApp({
  apiKey: 'your-app-id',
  host: 'post-purchase',     // sentinel — SDK resolves to current origin
});

const { order } = await app.dispatchAndWait('ORDER_GET');
The BRIDGE_PING handshake responds with { ok: true, host: 'post-purchase' } so a single extension iframe can detect which host it’s running in and swap behaviour:
const reply = await app.dispatchAndWait('BRIDGE_PING');
if (reply.host !== 'post-purchase') {
  // Same iframe URL is reused in /checkout — branch here
}

Capability table

ActionPost-purchase hostNotes
BRIDGE_PINGyes{ ok: true, host: 'post-purchase' }
ORDER_GETyesRead-only snapshot of the placed order
CUSTOMER_GETyesCustomer’s email (same shape as checkout)
CURRENCY_GETyesOrder currency ISO code
CART_LINES_CHANGEyes (addCartLine only)Creates a follow-on order tied to the original
REDIRECTyesSend the buyer to a URL (e.g. account, custom thank-you)
DONEyesFinish post-purchase flow and redirect to /orders/<id>
CLIPBOARD_WRITEyesiframe-side write (no host round-trip needed)
TOAST_SHOWnoUse in-iframe UI for messaging
MODAL_OPEN / MODAL_CLOSEnoWould block the buyer; render inline
RESOURCE_PICKER_*noAdmin-only
TITLE_BAR_UPDATE, NAV_MENU_UPDATEnoNo admin chrome here
CONTEXTUAL_SAVE_BARnoPost-purchase has no dirty-state flow
SESSION_TOKEN_REQUESTnoCustomer context — no merchant JWT
BUYER_JOURNEY_INTERCEPT_*noThe buyer has already placed the order
CART_GET, DISCOUNT_CODE_CHANGE, NOTE_CHANGE, ATTRIBUTE_CHANGEnoOriginal cart is sealed; use ORDER_GET
SHIPPING_ADDRESS_GET, DELIVERY_GROUPS_GETnoOrder has already shipped to the captured address
METAFIELD_CHANGEnoCart metafields are migrated; mutate via REST if needed
FULLSCREEN_*, SCANNER_*, PRINT, SHARE, HISTORY_*noNot wired for the post-purchase surface
USER_FETCH, CONFIG_FETCH, ENVIRONMENT_FETCH, FEATURES_QUERYnoAdmin-only data APIs
Unsupported actions return no response — the SDK call rejects with a 10s timeout. See Error Handling for the recovery pattern.

Action reference

BRIDGE_PING

Capability handshake. Always responds with the host identifier so a shared iframe URL can branch by host.
const reply = await app.dispatchAndWait('BRIDGE_PING');
// { ok: true, host: 'post-purchase' }

ORDER_GET

Read the order that was just placed. The shape mirrors the Shopify-compatible order envelope returned by the storefront API.
const { order } = await app.dispatchAndWait('ORDER_GET');
// {
//   id:           'gid://customerlms/Order/abc',
//   orderNumber:  '#1042',
//   totalPrice:   { amount: 9429.40, currencyCode: 'INR' },
//   subtotal:     { amount: 9600.00, currencyCode: 'INR' },
//   lineItems: [
//     { id, productId, variantId, title, quantity, price, image, … },
//     // …
//   ],
//   shippingAddress: { firstName, lastName, address1, … },
//   email:        'alex@example.com',
// }
Use the line items to compute relevant upsells; use totalPrice to present a “complete your order” CTA priced in the same currency.

CUSTOMER_GET

const { email } = await app.dispatchAndWait('CUSTOMER_GET');
// { email: 'alex@example.com' }
The customer is identified by the email captured during checkout. If the customer was logged in, the order is linked to their Customers row, but the post-purchase iframe is not given the customer id directly — look it up via order.customerId from ORDER_GET if you need to read additional data via REST.

CURRENCY_GET

const { currency } = await app.dispatchAndWait('CURRENCY_GET');
// { currency: 'INR' }
Identical to ORDER_GET().totalPrice.currencyCode. Exposed separately so extensions that only need currency don’t have to fetch the full order object.

CART_LINES_CHANGE (post-purchase upsell)

Post-purchase extensions can add line items, which the host packages as a follow-on order that bills to the same payment method as the original. Only addCartLine is supported.
import { Cart } from '@launchmystore/app-bridge';

const cart = Cart.create(app);
await cart.applyCartLinesChange({
  type: 'addCartLine',
  merchandiseId: 'gid://customerlms/Variant/upsell-variant',
  quantity: 1,
});
Updates and removes are rejected with error: 'not supported in post- purchase'. To roll back a mistaken add, call DONE without further interaction — the buyer will see only the original order on the next page. When the addCartLine targets a variant already on the original order, the new line is enriched from existing data (title, image, price) so the post-purchase UI doesn’t render placeholder content while the host re-runs verifyCart.

REDIRECT

Send the buyer somewhere other than the default order-status page. Useful for “claim your reward” flows or surveys.
import { Redirect } from '@launchmystore/app-bridge';

Redirect.create(app, { url: '/account?survey=post-purchase' }).dispatch();
External redirects work too — pass external: true to short-circuit the same-origin assertion:
app.dispatch('REDIRECT', {
  url: 'https://reviews.example.com/leave/' + order.orderNumber,
  external: true,
  newTab: false,
});
If you redirect, do not call DONE — the host treats the redirect as the terminal action.

DONE

Finish the post-purchase flow and forward the buyer to /orders/<id> (the standard order confirmation page). The action is fire-and-forget; the host immediately removes the iframe.
app.dispatch('DONE');
Always call DONE when your extension finishes — without it the iframe stays mounted indefinitely. If the user adds an upsell, complete the async settlement first and then dispatch DONE:
async function buyUpsell(variantId) {
  await cart.applyCartLinesChange({ type: 'addCartLine', merchandiseId: variantId, quantity: 1 });
  app.dispatch('DONE');
}

async function skip() {
  app.dispatch('DONE');
}

CLIPBOARD_WRITE

Same iframe-side path used in the admin host. Useful for “copy your order number” style affordances on the post-purchase page.
import { Clipboard } from '@launchmystore/app-bridge';

await Clipboard.write(app, order.orderNumber);
Clipboard.read() is not available — the host doesn’t broker CLIPBOARD_READ_REQUEST for the post-purchase surface. Plan UIs that only need write (copy invoice number, copy support link).

Iframe resize

Like the checkout host, the post-purchase host accepts APP_BRIDGE_RESIZE messages and clamps the iframe height to a reasonable range. Use a ResizeObserver for responsive content:
new ResizeObserver(() => {
  parent.postMessage(
    { type: 'APP_BRIDGE_RESIZE', height: document.documentElement.scrollHeight },
    '*',
  );
}).observe(document.body);

Worked example: post-purchase upsell with skip

<!doctype html>
<html><body>
  <h1>One more thing?</h1>
  <p id="title">Loading…</p>
  <button id="add">Add to my order</button>
  <button id="skip">No thanks</button>

  <script type="module">
    import { createApp, Cart, Redirect } from 'https://cdn.launchmystore.io/app-bridge/v1.mjs';

    const app  = createApp({ apiKey: 'upsell-app', host: 'post-purchase' });
    const cart = Cart.create(app);

    const UPSELL_VARIANT = 'gid://customerlms/Variant/gift-wrap';

    const { order } = await app.dispatchAndWait('ORDER_GET');
    document.getElementById('title').textContent =
      `Add gift wrap to order #${order.orderNumber} for ${order.totalPrice.currencyCode} 50.00?`;

    document.getElementById('add').onclick = async () => {
      try {
        await cart.applyCartLinesChange({
          type: 'addCartLine',
          merchandiseId: UPSELL_VARIANT,
          quantity: 1,
        });
      } catch (err) {
        console.error('upsell failed', err);
      }
      app.dispatch('DONE');
    };

    document.getElementById('skip').onclick = () => app.dispatch('DONE');

    new ResizeObserver(() => parent.postMessage(
      { type: 'APP_BRIDGE_RESIZE', height: document.documentElement.scrollHeight },
      '*',
    )).observe(document.body);
  </script>
</body></html>

Persistence and side effects

Two things to be aware of about state that survives the post-purchase flow:
  • Lines added via CART_LINES_CHANGE create a real order. The new order is linked to the original via the customer record and uses the same payment method (no re-prompt). If your extension dispatches DONE immediately after the add, the buyer lands on /orders/<originalId> which renders both the original line items and the follow-on order as a single combined receipt.
  • REDIRECT skips the order page. If you redirect away, your extension owns the “thank you” experience. The order is still placed — but the buyer never sees the default confirmation page unless your destination links back to it.
Side-effects that the post-purchase host does not trigger:
  • No webhook re-emit. orders/create fires when the original order is placed (pre-bridge). Upsell adds emit a fresh orders/create for the follow-on order, then orders/updated on the original to reflect the linkage.
  • No discount re-eval on the original. Adding upsell lines does not re-run the original order’s cart_transform / discount functions. Apply discounts as merchant rules before the buyer reaches post-purchase if they need to gate on the upsell.

Security

  • Post-purchase iframes are same-origin with the storefront. The host treats every same-origin frame as trusted; do not rely on the message channel for authentication.
  • Customer PII is limited to what the buyer entered at checkout — email
    • shipping address only. Payment details are never exposed.
  • The customer is not necessarily logged in. Do not assume email maps to a logged-in Customers row — guests reach post-purchase via the email captured during checkout.

Troubleshooting

dispatchAndWait('ORDER_GET') times out. The host hasn’t mounted yet. Wait for BRIDGE_PING to resolve once before issuing real calls, or retry the read once on the first timeout. Iframe never goes away after I call DONE. Confirm you’re dispatching to the right host. app.dispatch('DONE') posts a message to window.parent; if your iframe was opened in a new tab (not embedded by the host) there is no parent to receive it. My upsell add succeeded but the buyer didn’t see the new line. The host removes the iframe immediately on DONE. The combined receipt renders on /orders/<id> once the follow-on order finishes settling — that can take a second on slow networks. If you want a visible “Adding…” affordance, await the applyCartLinesChange promise before dispatching DONE. CART_LINES_CHANGE with updateCartLine returns an error. Post-purchase only supports addCartLine. Update / remove operations are rejected on this host.

See Also