Skip to main content

POS Extensions

POS extensions let your app inject UI into the LaunchMyStore Point-of-Sale (POS) app — the in-person sales surface staff use at a counter. Like admin blocks, POS extensions declare sandboxed iframes at named slots inside the POS UI.
All six targets below render in the POS app. Each one is mounted at a real slot in the register UI — see the Targets table for exactly where. Extensions declared at any other pos. target are still installed and returned by the discovery API, but nothing mounts them.
POS extensions share the manifest shape and install pipeline with admin blocks. The only difference is the target prefix (pos.<surface>.<placement>.render) and a flag the host uses to branch rendering between admin and POS surfaces.
The POS host renders extensions only while a shift is open. The POS tool shows the open-shift prompt instead of the register UI until a cashier opens a drawer, so every POS slot is unmounted before that point. Don’t treat a missing iframe on a fresh POS session as an install failure.

When to use POS extensions

Use a POS extension when staff at the register need information or actions that aren’t part of the default POS flow:
  • Look up a loyalty member by phone and apply their tier discount
  • Check warehouse stock from a partner system before promising backorder
  • Print a custom gift receipt with a per-item warranty card
  • Show recent purchases for the current customer
If your app only needs to add screens to the merchant admin (no register flow), use Admin Blocks or Admin Actions instead.

Manifest

POS extensions live under extensions.posExtensions in your app.json. The install endpoint also accepts entries in this array; the local manifest helper merges them with adminExtensions and tags POS entries with type: 'pos_extension'.

Manifest fields

string
required
Unique extension handle within this app. Used as the iframe id.
string
required
POS injection point — must start with pos.. The pos. prefix is what distinguishes a POS extension from an admin block, and it is preserved verbatim end to end (the API’s only target rewrite is treating a trailing .block.render as equivalent to .render), so a POS target can never match an admin slot or vice versa. See Targets below.
string
Tab/panel label shown in the POS UI. Defaults to the extension name or app name when omitted.
string
Absolute URL the iframe loads. Relative URLs (e.g. /extensions/.../tool.html) are absolutized against the LaunchMyStore origin before the response is sent — POS clients running on a different host resolve them correctly.
string
Small icon (SVG/PNG) shown next to the title in the POS tab strip.
string
Override for the tab label when it appears in a list/nav strip. Falls back to title when omitted.
object
Free-form permission flags forwarded to the host. Consumers (POS app) decide which gates to enforce.

Targets

All six targets below are mounted by the POS app. resourceId / resourceType query params are passed only where a resource is in scope — the three “detail” targets. The other three are ambient: they render with the store context but no resource id. The three modal-hosted targets mount only while their modal is open, and unmount when it closes — do not rely on a persistent iframe across resources. Targets are validated as “starts with pos.” at the API layer, so a custom pos.* target installs cleanly and is returned by the discovery API — but nothing renders it. Only the six targets above have a host slot.

Iframe contract

POS extensions follow the same iframe contract as Admin Blocks:
1

Host fetches the extension list

The POS host calls GET /api/apps/admin-extensions?target=pos.checkout.block.render&domainSlug=… with the merchant bearer token and receives the merged backend + local-manifest list for that exact target. Because the query is already target-scoped, no additional POS/admin separation is needed.
2

Host renders an iframe per extension

The host creates a sandboxed <iframe> for each entry with src = ext.url (absolutized against the LaunchMyStore origin when relative), extensionId = ext.id in the query string, and the target-defined permission flags.
3

Iframe initializes App Bridge

Inside the iframe, call createApp({ apiKey, host }) from @launchmystore/app-bridge to begin posting APP_BRIDGE_ACTION messages to the host (dispatch, dispatchAndWait, subscribe, getSessionToken). See App Bridge.
4

Resize negotiation

Send { type: 'APP_BRIDGE_RESIZE', extensionId, height } whenever the iframe’s content height changes. The extensionId MUST match the id the host passed in via ?extensionId= — otherwise the iframe stays at the 200px default and looks empty.

Install pipeline

POS extensions install through the same endpoint as every other extension. POST /api/apps/install-extensions is a server-to-server endpoint gated by the platform’s internal API key — it is normally invoked by the backend as part of the app install flow, not called directly from a browser with a merchant token. (The only browser-accepted variant is a fromCatalog: true catalog copy, which additionally requires the merchant to own the target store.)
The installer:
  1. Persists the array into app.json under extensions.posExtensions.
  2. Invalidates the local manifest cache so the next read picks up the new entries within the 60s TTL.
  3. Returns installed.posExtensions = <count> in the response.

Discovery API

POS hosts call the same admin-extensions endpoint with a pos. target:
Response (truncated):
Do not filter with &type=pos_extension. Only the file-manifest source tags POS entries that way. Rows coming from the backend registry are typed from the target’s verb segment instead — a pos.*.block.render target derives admin_block and pos.order.action.render derives admin_action — so a type=pos_extension filter silently drops every DB-installed POS extension while file-installed ones still appear. Filter on the target (which the API already does when you pass ?target=), or on the pos. prefix, never on type.

Security

The admin-extensions API requires a bearer token. Unauthenticated requests get 401 Missing bearer token with an empty extension list. This prevents leaking which apps a merchant has installed.
  • All iframes load over HTTPS in production and are sandboxed. Your extension is served from your own origin, so it is cross-origin to the POS host and cannot read the host’s cookies, storage or DOM.
  • App Bridge postMessage is gated by the host’s allowedOrigins list — only registered app origins can dispatch actions.
  • Relative url values are absolutized against the LaunchMyStore storefront origin before being returned — POS clients calling cross-origin still resolve the iframe correctly.

See also

Admin Blocks

Same iframe contract for merchant admin pages.

App Bridge

Iframe-to-host messaging SDK.

App Manifest

Every field in app.json.

App Bridge Actions

Resource picker (11 types including customer and product) and all other actions.