Reference

Extension points

Where apps attach inside POS for orders, products, purchases, exports and warehouse transfers, how each target is presented and what context data it receives.

View Markdown

Applies to api_version: "2026-01".

An extension point (target) is a place POS exposes for apps. Each target defines where it is shown, how it is presented, what context data the module receives and what it must return.

Naming

Targets attached to a POS record are named <resource>.<location>, for example order.details.action or purchase.index.selection-action. Every resource offers the same six locations, so an app written for orders works the same way for purchases or warehouse transfers.

Resource POS screen List Detail form
order Orders Order list Order form
product Products Product list Product modal
purchase Purchases (goods receipts) Purchase list Purchase modal
export Exports (goods issues) Export list Export modal
warehouse_transfer Warehouse transfers Transfer list Transfer modal

Target list

Target Location in POS Presentation
nav.item Main menu, "Apps" group Declared only, no code
app.page The app's page /shop/<shop_id>/apps/<handle>/<path> iframe or Worker UI
<resource>.details.action Action menu of the detail form headless, modal, fullscreen, navigate
<resource>.index.action Action menu on each list row headless, modal, fullscreen, navigate
<resource>.index.selection-action Bulk action bar when several records are selected headless, modal, fullscreen, navigate
<resource>.details.block A box inside the detail form Worker UI
<resource>.before_submit Before a record is created/updated on the detail form hook, headless
<resource>.after_save After a record is created/updated on the detail form hook, headless

<resource> is one of order, product, purchase, export, warehouse_transfer.

Planned: order.print-action (custom print templates), order.index.column (custom columns in the order list), customer.details.block.

Module convention

Every module is an ES module with a function as its export default. The function receives the context data (same as pos.data) and may be async.

TypeScript
// Extensions with UI (block, modal/fullscreen action, Worker UI page): render into document.body
export default async function (data) {
  render(<App data={data} />, document.body);
}

// Headless extensions and hooks: return the result expected by the target
export default async function (data) {
  return { message: 'Done' };
}

Shared types

Context data has the same shape for every resource: data.resource names the resource, data.record is the saved record (data.records in the bulk action bar) and data.draft is what the detail form currently shows.

TypeScript
type Resource = 'order' | 'product' | 'purchase' | 'export' | 'warehouse_transfer';

type RefOf<R extends Resource> = {
  order: OrderRef;
  product: ProductRef;
  purchase: PurchaseRef;
  export: ExportRef;
  warehouse_transfer: WarehouseTransferRef;
}[R];

type DraftOf<R extends Resource> = {
  order: OrderDraft;
  product: ProductDraft;
  purchase: PurchaseDraft;
  export: ExportDraft;
  warehouse_transfer: WarehouseTransferDraft;
}[R];

// Detail form: *.details.action, *.details.block, *.before_submit
type DetailsData<R extends Resource> = {
  resource: R;
  mode: 'create' | 'edit';
  record: RefOf<R> | null;   // null while the record is unsaved
  draft: DraftOf<R>;         // includes unsaved changes
};

// One list row: *.index.action
type IndexData<R extends Resource> = {
  resource: R;
  record: RefOf<R>;
};

// Bulk action bar: *.index.selection-action
type SelectionData<R extends Resource> = {
  resource: R;
  records: RefOf<R>[];       // up to 500 records
};

Example pos.data in purchase.details.action:

JSON
{
  "resource": "purchase",
  "mode": "edit",
  "record": { "id": "7c1d…", "display_id": 318, "custom_id": null, "status": 0, "warehouse_id": "a52f…", "updated_at": "2026-10-06T02:45:00" },
  "draft": { "status": 0, "warehouse_id": "a52f…", "supplier_products_id": "e09b…", "tags": [], "note": null, "items": [{ "variation_id": "41aa…", "quantity": 20, "imported_price": 1850000 }], "total_price": 37000000 }
}

Record id

Resource Id used with the Open API Query added by navigate
order record.system_id order_id, order_ids
product record.id product_id, product_ids
purchase record.id purchase_id, purchase_ids
export record.id export_id, export_ids
warehouse_transfer record.id warehouse_transfer_id, warehouse_transfer_ids

Types per resource

TypeScript
type OrderRef = {
  id: string | number;      // displayed order id (the custom id if the shop uses one, otherwise the system id)
  system_id: number;        // system order id, as the order's system_id field in the Open API: /shops/{shop_id}/orders/{system_id}
  status: number;
  updated_at: string;       // ISO 8601
};

type OrderDraft = {
  status: number;
  customer_id: string | null;
  bill_full_name: string | null;
  bill_phone_number: string | null;
  warehouse_id: string | null;
  tags: (number | string)[];
  note: string | null;
  items: {
    variation_id: string;
    product_id: string;
    quantity: number;
    price: number;          // unit price after line discount
  }[];
  total_price: number;
};

type ProductRef = {
  id: string;               // UUID: /shops/{shop_id}/products/{id}
  display_id: number;
  custom_id: string | null; // product code
  name: string;
  is_hide: boolean;
  updated_at: string;
};

type ProductDraft = {
  name: string;
  custom_id: string | null;
  category_ids: number[];
  brand_id: number | null;
  tags: (number | string)[];
  variations: {
    id: string | null;      // null for a variation not saved yet
    custom_id: string | null;
    barcode: string | null;
    retail_price: number;
  }[];
};

type PurchaseRef = {
  id: string;               // UUID: /shops/{shop_id}/purchases/{id}
  display_id: number;
  custom_id: string | null;
  status: number;
  warehouse_id: string;
  updated_at: string;
};

type PurchaseDraft = {
  status: number;
  warehouse_id: string | null;
  supplier_products_id: string | null;   // supplier
  tags: (number | string)[];
  note: string | null;
  items: {
    variation_id: string;
    quantity: number;
    imported_price: number;
  }[];
  total_price: number;
};

type ExportRef = {
  id: string;               // UUID: /shops/{shop_id}/export/{id}
  display_id: number;
  custom_id: string | null;
  status: number;
  type: number;             // export type as in the Open API (normal, defective goods…)
  warehouse_id: string;
  updated_at: string;
};

type ExportDraft = {
  status: number;
  type: number;
  warehouse_id: string | null;
  tags: (number | string)[];
  note: string | null;
  export_items: {
    variation_id: string;
    quantity: number;
  }[];
};

type WarehouseTransferRef = {
  id: string;               // UUID: /shops/{shop_id}/transfers/{id}
  display_id: number;
  custom_id: string | null;
  status: number;
  from_warehouse_id: string;
  to_warehouse_id: string;
  updated_status_at: string;
};

type WarehouseTransferDraft = {
  status: number;
  from_warehouse_id: string | null;
  to_warehouse_id: string | null;
  note: string | null;
  items: {
    variation_id: string;
    quantity: number;
  }[];
};
  • Field names and status codes follow the Open API. Fields the user may not see (for example the imported price without the matching permission) are left out, and fields listed in access.hidden_order_fields are removed from OrderDraft (their value becomes undefined).
  • Context data only includes the minimum fields. Call the Open API with pos.api when you need more (see Runtime API).
  • If a record does not satisfy the app's access limits (warehouse, product category, order source…), POS does not show the extension for that record.

Adds an item to the main menu that opens an app.page.

Manifest Description
label Label in the menu
page id of the app.page extension

No module. The item is only shown to people who can see the app (every shop member by default, see App lifecycle).

  1. The app adds an item to the POS main menu
  2. Hover to see the app pages, click to open one
  3. The app page runs in an iframe on a sandbox domain and reads data through the Open API
  4. Switching tabs inside the app updates the POS URL
IllustrationApp menu item and pagenav.itemapp.page

app.page

The app's own page.

Manifest Description
title Page title
render iframe: free-form SPA; worker: Worker UI with pos-* elements
entry HTML file in the bundle (with iframe)
url Self-hosted page on your own server (with iframe, instead of entry)
module Module (with worker)

Context data:

TypeScript
type PageData = {
  path: string;                     // the part after /apps/<handle>, for example "/returns"
  query: Record<string, string>;    // the POS URL query string
};
  • Iframe pages: the URL is synced both ways; when the app changes its internal route the POS URL follows, so reloads, the back button and shared links all work. The page connects to POS through the SDK, see Runtime API › Iframe pages.
  • Worker UI pages: the worker has no history. Change the path with pos.navigate({ page, path, query }); POS updates the URL and emits data-change with the new path and query without recreating the worker. The browser back button also emits data-change.

*.details.action

Adds an item to the action menu of a detail form. App items sit in an "Apps" group at the bottom of the menu, in declaration order.

Resource Menu
order The action menu opened from the Save button of the order form
purchase, export, warehouse_transfer The status menu in the footer of the detail modal
product An "Apps" button in the product modal header, shown only when an app adds actions there

Context data: DetailsData<R>.

For orders, app items are not shown when the menu is in print-only mode or actions are hidden.

Action types

presentation On click Module returns
headless Runs the module in the worker; the menu item shows a running state { message?: string, refresh?: boolean } or nothing
modal Opens a medium modal; the module renders its content Nothing; call pos.close() to close
fullscreen Opens a full-screen modal; closing returns to the same screen Same as modal
navigate Navigates to page at path, adding the record id to the query (see Record id) No module

For headless:

  • If it returns message, POS shows a success toast with that text.
  • If it throws (throw new Error('...')), POS shows an error toast with error.message.
  • refresh defaults to true: POS reloads the record when the module finishes.
  • Maximum run time is 30 seconds; after that the worker is stopped and an error is reported.

The videos below use orders; other resources behave the same way.

  1. Open an order and hover the Save button to open the action menu
  2. App actions sit in the Apps group of the menu
  3. Code runs in the worker and calls the Open API and the ERP
  4. When done, POS shows a notification and reloads the order
IllustrationHeadless action in the order action menuorder.details.actionpresentation: headless
  1. Pick the Warranty card action in the action menu
  2. POS opens a modal whose content the app draws with pos-* elements
  3. Fill it in like any POS form
  4. The app calls pos.close() and shows a notification when done
IllustrationAction that opens a modalorder.details.actionpresentation: modal
  1. fullscreen: a multi-step flow in a full-screen modal
  2. Closing returns to the same order
  3. navigate: opens the app page with order_id
IllustrationFull-screen and navigate actionsorder.details.actionpresentation: fullscreenpresentation: navigate

should_render

The should_render module receives the same context data and returns a boolean.

  • POS evaluates it ahead of time when the record loads or changes, caching the result by record id and its update time (updated_status_at for warehouse transfers).
  • Timeout is 300 ms; on timeout or error the item is hidden.
  • Prefer conditions in the manifest; only use should_render for complex conditions.

*.index.action

Like *.details.action, but on each row of the list.

Resource Menu
order, purchase, export, warehouse_transfer The status menu on each row
product A "⋯" menu at the end of each row, shown only when an app adds actions there

Context data: IndexData<R>.

  1. Hover an order's status in the list
  2. The app action sits at the bottom of the status menu
  3. When it finishes, the row gets a tag and POS shows a notification
IllustrationAction on each order list roworder.index.action

*.index.selection-action

An item on the action bar shown when several records are selected in the list.

Context data: SelectionData<R>. With navigate, the query contains every id, for example purchase_ids=<id>,<id>,....

  1. Select several orders in the list
  2. The bulk action bar shows the app action
  3. The app receives order_ids and opens its page
IllustrationAction for multiple selected ordersorder.index.selection-actionpresentation: navigate

*.details.block

A box inside the detail form.

Resource Position
order Right after the Note section
product, purchase, export, warehouse_transfer At the end of the detail modal, above the footer buttons
  • Context data: DetailsData<R> (same as *.details.action).
  • When the user edits the form, POS updates pos.data and emits a data-change event (see Runtime API › Events).
  • The box is as wide as its column; its height grows with its content.
  • The box never blocks the form: POS shows a skeleton while the worker starts.
  1. The app's box sits right inside the order form
  2. POS shows a skeleton while the worker starts
  3. Editing the order sends data-change and the box updates instantly
IllustrationBlock in the order formorder.details.block

*.after_save

Runs after a record is successfully created or updated on its detail form in POS web.

TypeScript
type AfterSaveInput<R extends Resource> = {
  resource: R;
  mode: 'create' | 'edit';
  record: RefOf<R>;
};
  • Does not block the user; the return value is ignored.
  • Errors only go to the app's error log and are not shown to the user.
  1. Save the order as usual
  2. After saving, POS notifies the app through order.after_save
  3. The hook runs in the background; the user never waits
IllustrationHook after savingorder.after_save

*.before_submit

Runs before a record is created or updated on its detail form in POS web.

TypeScript
type BeforeSubmitInput<R extends Resource> = DetailsData<R>;

type BeforeSubmitResult =
  | { ok: true }
  | { ok: false; message: string };   // message up to 200 characters, shown to the user
  • Hooks from several apps run in parallel. If any hook returns ok: false, the record is not saved; POS shows every message.
  • When timeout_ms is exceeded or the module fails, on_error decides (allow saves, block stops the save).
  • This version can only block; it cannot modify the record.
  1. On Save, POS runs the app's hook before saving
  2. The hook returns ok: false, so the order is not saved
  3. Add the phone number and save again
  4. The hook returns ok: true and POS saves the order as usual
IllustrationValidation hook before savingorder.before_submit

When hooks run

*.before_submit and *.after_save only run when the user saves on the detail form in POS web. They do not run for changes made elsewhere: the status menu on list rows, bulk updates, Excel import, the Open API, the mobile app or marketplace sync. Rules that must always hold belong on the server.