> ## 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

> App Bridge action subset available to post-purchase extension iframes

# Post-Purchase Bridge

Post-purchase extensions render between **order-placement** and the
order-confirmation page. They run as same-origin iframes inside
LaunchMyStore 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](/extensions/post-purchase).

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

```js theme={null}
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:

```js theme={null}
const reply = await app.dispatchAndWait('BRIDGE_PING');
if (reply.host !== 'post-purchase') {
  // Same iframe URL is reused in /checkout — branch here
}
```

## Capability table

| Action                                                                | Post-purchase host       | Notes                                                    |
| --------------------------------------------------------------------- | ------------------------ | -------------------------------------------------------- |
| `BRIDGE_PING`                                                         | yes                      | `{ ok: true, host: 'post-purchase' }`                    |
| `ORDER_GET`                                                           | yes                      | Read-only snapshot of the placed order                   |
| `CUSTOMER_GET`                                                        | yes                      | Customer's email (same shape as checkout)                |
| `CURRENCY_GET`                                                        | yes                      | Order currency ISO code                                  |
| `CART_LINES_CHANGE`                                                   | yes (`addCartLine` only) | Creates a follow-on order tied to the original           |
| `REDIRECT`                                                            | yes                      | Send the buyer to a URL (e.g. account, custom thank-you) |
| `DONE`                                                                | yes                      | Finish post-purchase flow and redirect to `/orders/<id>` |
| `CLIPBOARD_WRITE`                                                     | yes                      | iframe-side write (no host round-trip needed)            |
| `TOAST_SHOW`                                                          | no                       | Use in-iframe UI for messaging                           |
| `MODAL_OPEN` / `MODAL_CLOSE`                                          | no                       | Would block the buyer; render inline                     |
| `RESOURCE_PICKER_*`                                                   | no                       | Admin-only                                               |
| `TITLE_BAR_UPDATE`, `NAV_MENU_UPDATE`                                 | no                       | No admin chrome here                                     |
| `CONTEXTUAL_SAVE_BAR`                                                 | no                       | Post-purchase has no dirty-state flow                    |
| `SESSION_TOKEN_REQUEST`                                               | no                       | Customer context — no merchant JWT                       |
| `BUYER_JOURNEY_INTERCEPT_*`                                           | no                       | The buyer has already placed the order                   |
| `CART_GET`, `DISCOUNT_CODE_CHANGE`, `NOTE_CHANGE`, `ATTRIBUTE_CHANGE` | no                       | Original cart is sealed; use `ORDER_GET`                 |
| `SHIPPING_ADDRESS_GET`, `DELIVERY_GROUPS_GET`                         | no                       | Order has already shipped to the captured address        |
| `METAFIELD_CHANGE`                                                    | no                       | Cart metafields are migrated; mutate via REST if needed  |
| `FULLSCREEN_*`, `SCANNER_*`, `PRINT`, `SHARE`, `HISTORY_*`            | no                       | Not wired for the post-purchase surface                  |
| `USER_FETCH`, `CONFIG_FETCH`, `ENVIRONMENT_FETCH`, `FEATURES_QUERY`   | no                       | Admin-only data APIs                                     |

Unsupported actions return no response — the SDK call rejects with a 10s
timeout. See [Error Handling](/app-bridge/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.

```js theme={null}
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 order
envelope returned by the storefront API.

```js theme={null}
const { order } = await app.dispatchAndWait('ORDER_GET');
// {
//   id:           'gid://launchmystore/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`

```js theme={null}
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`

```js theme={null}
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.

```js theme={null}
import { Cart } from '@launchmystore/app-bridge';

const cart = Cart.create(app);
await cart.applyCartLinesChange({
  type: 'addCartLine',
  merchandiseId: 'gid://launchmystore/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 cart verification.

### `REDIRECT`

Send the buyer somewhere other than the default order-status page.
Useful for "claim your reward" flows or surveys.

```js theme={null}
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:

```js theme={null}
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.

```js theme={null}
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`:

```js theme={null}
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.

```js theme={null}
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:

```js theme={null}
new ResizeObserver(() => {
  parent.postMessage(
    { type: 'APP_BRIDGE_RESIZE', height: document.documentElement.scrollHeight },
    '*',
  );
}).observe(document.body);
```

## Worked example: post-purchase upsell with skip

```html theme={null}
<!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://launchmystore/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

* [Post-Purchase Extensions](/extensions/post-purchase) — manifest shape,
  lifecycle, sandbox flags.
* [Actions Reference](/app-bridge/actions) — full action catalogue
  including the admin-only entries.
* [App Bridge for Checkout](/app-bridge/checkout) — the larger sibling
  surface (`host: 'checkout'`).
* [Error Handling](/app-bridge/error-handling) — timeout + rejection
  patterns shared by all hosts.
