Reference

Runtime API (pos)

The pos object in extensions and iframe pages: context, API calls, toasts, navigation and events.

View Markdown

Applies to api_version: "2026-01".

Every extension gets a pos object to read context, call the Open API and send commands to the POS UI.

  • Extensions in the worker (blocks, actions, hooks, Worker UI pages): pos is a global preloaded by the POS runtime before the module runs.
  • Iframe pages: call connect() from the SDK to get pos (see Iframe pages).

Full types ship in @pancake-pos/ui-extensions (see SDK & CLI).

Overview

TypeScript
interface Pos {
  apiVersion: string;                 // for example "2026-01"
  target: string;                     // the running extension's target
  extensionId: string;                // the id declared in the manifest
  app: { id: string; handle: string; version: string };
  shop: { id: number; name: string; currency: string; country: string; timezone: string };
  user: { id: string; name: string; locale: string };
  data: unknown;                      // context data, schema depends on the target

  api: PosApi;                        // Open API calls
  session: { getToken(): Promise<{ token: string; expiresAt: string }> };

  toast: { show(message: string, options?: ToastOptions): void };
  ui: { confirm(options: ConfirmOptions): Promise<boolean> };
  navigate(to: NavigateTarget): void;
  refresh(): void;
  close(): void;

  on(event: PosEvent, handler: (payload: any) => void): () => void;
}

Availability

API Block Headless action Modal/fullscreen action Hook, should_render Page
shop, user, app, data ✓ ✓ ✓ ✓ ✓
api, session ✓ ✓ ✓ ✓ ✓
toast, ui.confirm ✓ ✓ ✓ ✓
navigate ✓ ✓ ✓ ✓
refresh ✓ ✓ ✓
close ✓
on('data-change') ✓ ✓ ✓
page.* ✓ (iframe)

Calling an API outside its availability throws PosRuntimeError with code: 'unavailable'.

pos.data

The context data of the current target, also passed to the export default function. See each target's schema in Extension points.

  • Targets on POS records share one shape: pos.data.resource names the resource (order, purchase…), pos.data.record is the saved record (records in the bulk action bar) and pos.data.draft is the detail form content.
  • Fields hidden by access.hidden_order_fields are not present in pos.data.
  • For blocks, pos.data updates as the user edits the form; listen to data-change to re-render.

pos.api

Calls the Open API with the app token. The SDK attaches the token, renews it and retries once when it expires.

TypeScript
interface PosApi {
  baseUrl: string;   // Open API root, for example "https://pos.pages.fm/api/v1"
  request<T = any>(options: {
    method: 'GET' | 'POST' | 'PUT' | 'DELETE';
    path: string;                          // relative to baseUrl, starts with /shops/{shop_id}
    query?: Record<string, unknown>;
    body?: unknown;                        // sent as JSON
    signal?: AbortSignal;
  }): Promise<T>;
  get<T = any>(path: string, options?: { query?: Record<string, unknown>; signal?: AbortSignal }): Promise<T>;
  post<T = any>(path: string, body?: unknown, options?: { query?: Record<string, unknown>; signal?: AbortSignal }): Promise<T>;
  put<T = any>(path: string, body?: unknown, options?: { query?: Record<string, unknown>; signal?: AbortSignal }): Promise<T>;
  delete<T = any>(path: string, options?: { query?: Record<string, unknown>; signal?: AbortSignal }): Promise<T>;
}
  • path must belong to the current shop (/shops/${pos.shop.id}/...). Calls to other shops are rejected.
  • The result is the raw Open API JSON (usually { success, data, ... }).
  • The method must be listed in the app's access.methods; otherwise an error is thrown before the request is sent.

Errors

TypeScript
class PosApiError extends Error {
  status: number;            // HTTP status
  code?: number | string;    // Open API error_code when present
  body?: unknown;            // response body
}

The SDK throws PosApiError when the HTTP status is ≥ 400 or the body has success: false. See the error codes in Data access › Error codes.

TypeScript
try {
  const { record } = pos.data;   // order.details.action: record is an OrderRef
  const res = await pos.api.get(`/shops/${pos.shop.id}/orders/${record.system_id}`);
  render(<OrderInfo order={res.data} />, document.body);
} catch (error) {
  if (error.status === 403) pos.toast.show('You do not have permission to view this order', { tone: 'error' });
  else throw error;
}

pos.session.getToken

Gets the current app token to call the Open API with another library.

TypeScript
const { token, expiresAt } = await pos.session.getToken();
await fetch(`${pos.api.baseUrl}/shops/${pos.shop.id}/orders`, {
  headers: { Authorization: `Bearer ${token}` },
});

Do not store the token long term or send it to external systems, except to the backend of a self-hosted page. Tokens expire after 15 minutes; call getToken() again for a new one.

pos.toast.show

TypeScript
type ToastOptions = {
  tone?: 'success' | 'error' | 'info' | 'warning';   // default 'success'
  duration?: number;                                  // seconds, default 3, from 1 to 10
};

At most 3 toasts per 10 seconds per extension; extra toasts are dropped.

pos.ui.confirm

A confirmation dialog drawn by POS over the whole screen.

TypeScript
type ConfirmOptions = {
  title: string;
  content?: string;
  okText?: string;       // defaults to the POS "OK" label
  cancelText?: string;   // defaults to the POS "Cancel" label
  danger?: boolean;      // red confirm button
};

if (await pos.ui.confirm({ title: 'Push this order to the ERP?' })) {
  // ...
}

pos.navigate

Navigates to one of the app's pages.

TypeScript
type NavigateTarget = {
  page: string;                       // id of an app.page extension
  path?: string;                      // for example "/returns"
  query?: Record<string, string>;
};

pos.navigate({ page: 'home', path: '/returns', query: { order_id: String(pos.data.record.system_id) } });

When called from a modal, the modal closes before navigating. If that page is already open, POS only updates the URL and emits data-change with the new path and query; the page is not reloaded.

pos.refresh

Asks POS to reload the data of the current screen (for example the open order or purchase) after the app updated it through the Open API.

  • POS reloads the list on screen and the open record. Unsaved changes in that record's form are replaced by the new data.
  • Repeated calls are merged: each resource reloads at most once every 3 seconds.

pos.close

Closes the open modal/fullscreen action.

Events

TypeScript
const off = pos.on('data-change', (data) => {
  // data has the same schema as pos.data
});
off(); // unsubscribe
Event Fires when Payload
data-change Context data changes, for example the user edits a detail form (batched over 300 ms) or a page's path changes the new pos.data
primary-action The user clicks the iframe page's primary button none

Iframe pages

A render: "iframe" page is a regular web app served from the root of the app's sandbox domain. Every path that is not a static file returns index.html, so routers use / as their basename. Paths starting with /_pos/ are reserved for POS: do not use them as routes or file paths.

TypeScript
import { connect } from '@pancake-pos/ui-extensions/page';

const pos = await connect();   // waits for POS to send context and token
// pos.data: { path, query }

On top of the APIs above, iframe pages get:

TypeScript
interface PosPage {
  setTitle(title: string): void;
  setPrimaryAction(action: { label: string; disabled?: boolean; loading?: boolean } | null): void;
}

pos.page.setTitle('Reconciliation for May 12');
pos.page.setPrimaryAction({ label: 'Export Excel' });
pos.on('primary-action', exportExcel);
  • URL sync: the SDK watches history.pushState, replaceState and popstate inside the iframe and updates the POS URL. When a shared link is opened, the iframe loads the matching path.
  • Height: the iframe fills the POS content area; the page scrolls internally.
  • Look and feel: @pancake-pos/ui (POS theme and components) is recommended.
  • Storage: the iframe page's localStorage belongs to the app's sandbox domain only and is not shared with POS or other apps.

Self-hosted pages

Declare url instead of entry when the page already runs on your own server, such as an existing ERP web app. POS embeds url directly in the iframe; the bundle does not need a page/ folder.

JSON
{ "target": "app.page", "id": "home", "title": "ERP", "render": "iframe", "url": "https://erp.example.com/pos" }
  • Connecting: install @pancake-pos/ui-extensions and call connect() as a bundled page does; pos.page, pos.api, pos.toast and the rest work the same way.
  • Paths: the path part of url is the app root. /shop/<id>/apps/<handle>/orders/5 in POS maps to https://erp.example.com/pos/orders/5. When the page leaves that root (for example /login), the POS URL stays as it is. pos.data.path is the page's real path.
  • Allowing POS to embed it: send Content-Security-Policy: frame-ancestors https://pos.pancake.vn https://pos.pages.fm so only POS can embed the page. Do not rely on the page's own sign-in cookies: browsers may block cookies in cross-site iframes (Safari does by default).
  • Calling the API from your server: send the app token (pos.session.getToken()) with requests to your backend, and have the backend call the Open API with it (Authorization: Bearer). The token lasts 15 minutes and carries only the app's access; do not store it long term.
  • Review and changes: admins see url when they activate a version. Your server decides what the page shows and can change it at any time without a new version, so only use servers you control.
  • Trying it out: pos-app dev --page-url http://localhost:3000/pos replaces url with a server on your machine; Vite or Next hot reload keeps working inside the iframe.

Worker environment

Extensions in the worker can only rely on:

  • Standard JavaScript syntax and built-ins (ES2020).
  • fetch, Headers, Request, Response, AbortController, URL, URLSearchParams.
  • setTimeout, clearTimeout, setInterval, clearInterval.
  • console (only visible in development mode).
  • The minimal DOM provided by the POS runtime so Preact or plain JS can build pos-* elements: document.createElement, document.createTextNode, append, appendChild, insertBefore, removeChild, remove, setAttribute, removeAttribute, textContent, addEventListener, removeEventListener, parentNode, childNodes, firstChild, nextSibling, CustomEvent. document.body is the root of the extension's UI.

Do not use window, document.querySelector, localStorage, sessionStorage, canvas, DOM measurement (getBoundingClientRect…), alert/confirm/prompt, or load scripts from CDNs. These may not exist or may be locked down at any time.

Error log

  • Development mode: errors and console.* show up in the browser DevTools.
  • Active version: uncaught errors, render errors and timeouts are recorded in the app's error log, visible in the error log on the custom apps page.