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

> Communicate between your checkout extension and the customer checkout host

# App Bridge for Checkout

Checkout extensions render as iframes inside the customer-facing 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

| Capability                                           | Admin host                                  | Checkout host (LaunchMyStore)          | Post-purchase host (LaunchMyStore) |
| ---------------------------------------------------- | ------------------------------------------- | -------------------------------------- | ---------------------------------- |
| Wire format                                          | `APP_BRIDGE_ACTION` / `APP_BRIDGE_RESPONSE` | Same                                   | Same                               |
| SDK package                                          | `@launchmystore/app-bridge`                 | Same                                   | Same                               |
| `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:

<CodeGroup>
  ```javascript Minimal (no SDK) theme={null}
  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 },
        '*'
      );
    });
  }
  ```

  ```javascript Using @launchmystore/app-bridge theme={null}
  import { createApp } from '@launchmystore/app-bridge';

  const app = createApp({
    apiKey: 'your-app-id',
    host: btoa(location.ancestorOrigins?.[0] || location.origin),
  });

  const cart = await app.dispatchAndWait('CART_GET', {});
  ```
</CodeGroup>

## Action Reference

### BRIDGE\_PING

Capability handshake. Resolves with `{ ok: true, host: 'checkout' }` if the checkout host is listening.

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

```javascript theme={null}
await dispatchAndWait('TOAST_SHOW', {
  message: 'Gift wrap added',
  variant: 'success',  // or 'error'
});
```

| Field     | Type                     | Required | Description                                     |
| --------- | ------------------------ | -------- | ----------------------------------------------- |
| `message` | string                   | Yes      | Up to 200 chars (longer messages are truncated) |
| `variant` | `'success'` \| `'error'` | No       | Default `success`                               |

Response: `{ ok: true }`

### CART\_GET

Read the current cart contents.

```javascript theme={null}
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 the verified cart. This is the same data the host renders in the order summary, so values stay consistent.

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

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

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

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

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

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

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

```javascript theme={null}
const { address } = await dispatchAndWait('BILLING_ADDRESS_GET');
```

### DISCOUNT\_CODES\_GET

Coupon codes the customer has applied to this cart.

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

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

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

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

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

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

```javascript theme={null}
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. The host stores the note on the cart and includes it in the order payload at placement.

### COUPON\_APPLY\_REQUEST

Forward a coupon code to the host's coupon input.

```javascript theme={null}
await dispatchAndWait('COUPON_APPLY_REQUEST', { code: 'WELCOME10' });
// { ok: true, code: 'WELCOME10' }
```

| Field  | Type   | Required | Description           |
| ------ | ------ | -------- | --------------------- |
| `code` | string | Yes      | Truncated 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 the cart, re-runs cart verification, 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.

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

```js theme={null}
// Add a new line
await cart.applyCartLinesChange({
  type: 'addCartLine',
  merchandiseId: 'gid://launchmystore/Variant/abc-123',
  quantity: 1,
  attributes: [{ key: 'gift_wrap', value: 'true' }],
});

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

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

// Batch
await cart.applyCartLinesChange([
  { type: 'addCartLine', merchandiseId: 'gid://...', quantity: 1 },
  { type: 'removeCartLine', id: 'gid://...' },
]);
```

| Field           | Type                                                    | Required          | Notes                                          |
| --------------- | ------------------------------------------------------- | ----------------- | ---------------------------------------------- |
| `type`          | `'addCartLine' \| 'updateCartLine' \| 'removeCartLine'` | yes               |                                                |
| `merchandiseId` | string (gid)                                            | for `addCartLine` | `gid://launchmystore/Variant/<uuid>`           |
| `id`            | string (gid)                                            | for update/remove | `gid://launchmystore/CartLine/<uuid>`          |
| `quantity`      | integer                                                 | for add/update    | Updates with `0` remove the line               |
| `attributes`    | `{key,value}[]`                                         | no                | Per-line properties — stored on the order line |

Response:

```ts theme={null}
{
  ok: true,
  results: Array<{ type: string, ok: boolean, error?: string }>,
}
```

Internally the host applies the add / update-quantity / remove
mutation to the cart and re-fetches the cart envelope.
When `addCartLine` targets a variant already in the order summary, the
new line is enriched from existing cart / verified-cart 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.

```js theme={null}
await cart.applyDiscountCodeChange({
  type: 'addDiscountCode',
  code: 'WELCOME10',
});
```

| Field  | Type                                        | Required | Notes                                                               |
| ------ | ------------------------------------------- | -------- | ------------------------------------------------------------------- |
| `type` | `'addDiscountCode' \| 'removeDiscountCode'` | yes      | `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.

### NOTE\_CHANGE

Update or remove the cart note.

```js theme={null}
await cart.applyNoteChange({
  type: 'updateNote',
  note: 'Leave with concierge — apt 4B',
});

await cart.applyNoteChange({ type: 'removeNote' });
```

| Field  | Type                           | Required         | Notes                  |
| ------ | ------------------------------ | ---------------- | ---------------------- |
| `type` | `'updateNote' \| 'removeNote'` | yes              |                        |
| `note` | string                         | for `updateNote` | Truncated to 500 chars |

The note is stored on the cart and persisted to the order's `note`
field at placement.

### ATTRIBUTE\_CHANGE

Set or remove a cart-level attribute (key/value pair stored on the order).

```js theme={null}
await cart.applyAttributeChange({
  type: 'updateAttribute',
  key: 'gift_wrap',
  value: 'true',
});

await cart.applyAttributeChange({
  type: 'removeAttribute',
  key: 'gift_wrap',
});
```

| Field   | Type                                     | Required   | Notes                 |
| ------- | ---------------------------------------- | ---------- | --------------------- |
| `type`  | `'updateAttribute' \| 'removeAttribute'` | yes        |                       |
| `key`   | string                                   | yes        | Truncated to 64 chars |
| `value` | any                                      | for update | JSON-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.

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

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

| Field     | Type                      | Required | Notes                                           |
| --------- | ------------------------- | -------- | ----------------------------------------------- |
| `type`    | `'updateShippingAddress'` | yes      | Only one type today                             |
| `address` | object                    | yes      | Field names match `SHIPPING_ADDRESS_GET` output |

The host merges incoming fields with the existing address (only non-empty values overwrite). After the write, cart verification 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:

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

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

| Field      | Type                 | Description                                |
| ---------- | -------------------- | ------------------------------------------ |
| `behavior` | `'allow' \| 'block'` | Required                                   |
| `reason`   | string               | Shown to the customer when `block`         |
| `perform`  | function             | Runs 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.

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

| Target                                              | Renders                                                   | Customer-facing use cases                                    |
| --------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------ |
| `checkout-contact-after`                            | After email/login row                                     | Delivery notes, account-creation prompts, newsletter signups |
| `checkout-shipping-after`                           | After address form                                        | Address validation, location-based offers                    |
| `checkout-payment-before`                           | Above payment methods                                     | Trust badges, payment-method explainers                      |
| `checkout-payment-after`                            | Below payment methods                                     | "Why you can trust us", security seals                       |
| `checkout-order-summary-before`                     | Top of order summary                                      | Coupon helpers, promo banners                                |
| `checkout-order-summary-after`                      | Bottom of order summary                                   | Live savings calculators, loyalty point preview              |
| `purchase.checkout.block.render`                    | Custom block placement                                    | General-purpose surfaces                                     |
| `purchase.thank-you.block.render`                   | Order status page right after checkout (first visit only) | Post-purchase upsells, review prompts                        |
| `purchase.order-status.block.render`                | Order status page, every visit                            | Delivery ETA widgets, returns initiators, FAQ                |
| `purchase.thank-you.cart-line-list.render-after`    | After the order summary line list (first visit only)      | Per-item upsells                                             |
| `purchase.order-status.cart-line-list.render-after` | After the order summary line list, every visit            | Warranty registration, reorder buttons                       |

## Worked Example: Delivery Instructions Card

A complete iframe that asks the customer for delivery instructions and saves them via `ORDER_NOTE_SET`.

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

```json theme={null}
{
  "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 stored on the cart, included in the order payload at placement, and saved to the order's `note` field.
* **`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 at cart verification and order placement. 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 when the checkout page mounts, so it's not listening immediately on load. 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 checkout extension slot already passes the correct sandbox flags.
