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

# Customer Account Extensions

> Inject app UI into the logged-in customer account pages

# Customer Account Extensions

Customer-account extensions are iframes injected into the customer's
account pages (`/account`, `/orders/[orderId]`, `/account/profile`).
Use them for order tracking widgets, loyalty balances, support chat
panels, and similar customer-facing surfaces.

These are distinct from **admin extensions** (which target the merchant
admin UI) and from **checkout extensions** (which target the
unauthenticated checkout flow). They run for a logged-in customer with
the customer's session.

## Targets

| Target                                       | Page                | Wired today |
| -------------------------------------------- | ------------------- | ----------- |
| `customer-account.order-status.block.render` | `/orders/[orderId]` | ✓ live      |
| `customer-account.dashboard.block.render`    | `/account`          | ✓ live      |
| `customer-account.order-list.block.render`   | `/account`          | ✓ live      |
| `customer-account.profile.block.render`      | `/account/profile`  | ✓ live      |

All four targets have a slot mounted in the storefront account
pages. `dashboard.block.render` renders above the page header on
`/account`; `order-list.block.render` renders between the header and the
order history list on the same page; `profile.block.render` renders above
the "Profile" heading on `/account/profile`; `order-status.block.render`
renders on the per-order page.

The `<CustomerAccountExtensionSlot target="..." />` component renders one
iframe per registered extension at the given target. It requires the
customer's session token as `bearerToken`; without it the slot API returns
401 and the slot renders empty (correct secure default — extension
enumeration is gated behind authentication).

## Manifest

```json theme={null}
{
  "type": "customer_account_extension",
  "handle": "order-tracker",
  "target": "customer-account.order-status.block.render",
  "url": "/extensions/<app>/order-tracker/index.html",
  "title": "Order Tracker",
  "permissions": ["read_orders"]
}
```

| Field         | Required | Description                                                          |
| ------------- | -------- | -------------------------------------------------------------------- |
| `target`      | yes      | One of the targets above.                                            |
| `url`         | yes      | Relative path (under `extensions/`) or absolute https URL.           |
| `title`       | no       | Label shown to the merchant in the apps admin.                       |
| `permissions` | no       | OAuth scopes the iframe needs — same vocabulary as admin extensions. |

## How the iframe is rendered

Each registered extension is rendered as a **plain iframe** pointed at
its resolved `url`:

```html theme={null}
<iframe
  src="<absolutized extension url>"
  sandbox="allow-scripts allow-forms allow-popups"
  scrolling="no"
  loading="lazy"
  height="120"
/>
```

A few things to design around:

* **No App Bridge host on this surface.** Unlike admin extensions, the
  customer-account slot does not run an App Bridge postMessage host, and
  it does not push any context object into the iframe. There is no
  `useApi()` / `EXTENSION_CONTEXT` wiring on the storefront side.
* **No page context is injected into the URL.** The slot mounts with the
  store's `domainSlug` and the customer's bearer token only — the host
  does **not** append `orderId` / `customerId` query parameters to your
  iframe `url`. If your extension needs to know which order or customer
  it's rendering for, fetch that from your own backend using the
  customer session token (see [Security](#security)), or derive it from
  the page your block targets.
* **Fixed height.** The iframe renders at a fixed 120px height. Keep
  your block compact, or scroll within it.

## Discovery

The storefront exposes:

```
GET /api/apps/customer-account-extensions?target=<target>&domainSlug=<slug>
```

which returns the registered extensions plus their absolutized URLs.
The slot component fetches this on mount and renders one iframe per
extension.

## Security

* The iframe is sandboxed with `allow-scripts allow-forms allow-popups`
  and **no** `allow-same-origin`, so it runs in an opaque origin and
  cannot read the parent page's cookies or storage.
* Enumerating a store's installed extensions is gated behind
  authentication: the discovery endpoint requires the customer's bearer
  token and returns `401` without it (the slot then renders nothing).
* The bearer token the slot is mounted with is the customer's session
  credential. If your iframe needs to call an API on behalf of the
  customer, your own backend must independently verify that credential —
  never trust ids passed to you in the clear.
