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

The storefront route resolves extensions from two sources and merges them:

| Source | Where it comes from |
| - | - |
| Installed-app record | Extensions you declared in `extensions.customerAccountExtensions` and registered through the dev portal or an app install. |
| On-disk manifest | Extensions written to `public/extensions/{domainSlug}/{appHandle}/` by an app install. |

Behind the storefront route, the platform resolves the first source via:

```
GET /apps/customer-account-extensions/{domainSlug}
Authorization: Bearer <customer session token>
```

This endpoint authenticates as the **customer** (not the merchant) and
only returns extensions belonging to apps that are actively installed on
that store and not suspended. A merchant admin token is rejected.

<Note>
  Extensions registered through the dev portal now render. Earlier builds
  had no backend resolver for this category, so only on-disk manifests were
  ever returned and portal-registered customer-account extensions stayed
  invisible with no error. If you registered one previously and never saw
  it, no manifest change is required — it resolves on its own.
</Note>

## 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
  token is validated by the platform on every call — a non-OK result
  yields no extensions rather than falling back to the on-disk manifests.
* 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.