# Manifest (pos-app.json)

`pos-app.json` lives at the project root. POS reads it on upload to learn which permissions the app needs, which domains it may call and where it attaches.

## Full example

```json
{
  "handle": "debt-reconciliation",
  "name": "Debt reconciliation",
  "description": "Checks customer debt when creating orders and reconciles at the end of the day.",
  "version": "1.3.0",
  "api_version": "2026-01",
  "access": {
    "methods": ["GET", "POST", "PUT"],
    "warehouse_ids": null,
    "hidden_order_fields": ["bill_phone_number"]
  },
  "network": {
    "allowed_origins": ["https://erp.example-customer.com"]
  },
  "extensions": [
    { "target": "nav.item", "id": "nav", "label": "Reconciliation", "page": "home" },
    { "target": "app.page", "id": "home", "title": "Debt reconciliation", "render": "iframe", "entry": "page/index.html" },
    {
      "target": "order.details.action",
      "id": "push-erp",
      "label": "Push to ERP",
      "presentation": "headless",
      "module": "extensions/push-erp.ts",
      "conditions": { "status": [1, 2] }
    },
    {
      "target": "order.details.action",
      "id": "warranty",
      "label": "Create warranty card",
      "presentation": "modal",
      "title": "Warranty card",
      "module": "extensions/warranty-modal.tsx",
      "should_render": "extensions/warranty-should-render.ts"
    },
    { "target": "order.details.block", "id": "debt", "module": "extensions/debt-block.tsx" },
    {
      "target": "purchase.index.action",
      "id": "push-purchase-erp",
      "label": "Push to ERP",
      "presentation": "headless",
      "module": "extensions/push-purchase-erp.ts",
      "conditions": { "status": [1] }
    },
    {
      "target": "order.before_submit",
      "id": "check-debt",
      "module": "extensions/check-debt.ts",
      "timeout_ms": 2000,
      "on_error": "allow"
    }
  ]
}
```

## Top-level fields

| Field | Type | Required | Description |
|---|---|---|---|
| `handle` | string | Yes | App identifier within the shop, matching `^[a-z0-9][a-z0-9-]{1,39}$`. Used in the app page URL. Changing `handle` makes the next upload create a separate new app. |
| `name` | string | Yes | Display name, up to 60 characters. |
| `description` | string | No | Short description, up to 300 characters. Shown to the admin when activating. |
| `version` | string | Yes | Semver, for example `1.3.0`. Every upload must use a version that does not exist yet. |
| `api_version` | string | Yes | The POS contract version (`YYYY-MM`). It determines the available targets, components and context schemas. |
| `access` | object | No | Limits on the data the app can use. See [access](#access). |
| `network` | object | No | External domains the app may call. See [network](#network). |
| `extensions` | array | Yes | Up to 30 extensions. See [extensions](#extensions). |

## access

**Additional** limits on top of the current user's permissions. An app token never has more permissions than the user; `access` only narrows them further.

| Field | Type | Default | Description |
|---|---|---|---|
| `methods` | string[] | `["GET"]` | Allowed HTTP methods: `GET`, `POST`, `PUT`, `DELETE`. When omitted the app is **read-only**. |
| `warehouse_ids` | uuid[] | no limit | Only operate on these warehouses. Applies to orders, purchases, exports, warehouse transfers and stock by warehouse. |
| `order_sources` | string[] | no limit | Only see orders from these sources. |
| `hidden_order_sources` | string[] | none hidden | Hide orders from these sources. |
| `order_statuses` | int[] | no limit | Only see orders in these statuses. |
| `order_since` | datetime (ISO 8601) | no limit | Only see orders created from this time on. |
| `hidden_order_fields` | string[] | none hidden | Remove these fields from order data returned to the app, including context data. |
| `product_categories` | int[] | no limit | Only see products in these categories. |
| `product_brands` | int[] | no limit | Only see products of these brands. |

`null` or an omitted field means no additional limit (except `methods`). The limits that apply are the `access` of the active version.

## network

| Field | Type | Description |
|---|---|---|
| `allowed_origins` | string[] | `https://` origins the app may call with `fetch`, up to 10, no wildcards. The POS Open API is always allowed and does not need to be listed. Also applies to `pos-image` sources. |

Requests to origins not on the list are blocked by the browser. External servers must enable CORS for the app's sandbox domain (see [Data access](/developers/apps/en/data-access#calling-external-systems)).

## extensions

### Common fields

| Field | Type | Required | Description |
|---|---|---|---|
| `target` | string | Yes | Where the extension attaches. See [Extension points](/developers/apps/en/targets). |
| `id` | string | Yes | Unique within the app, matching `^[a-z0-9-]{1,40}$`. |
| `module` | string | Depends on target | Source file path (`.ts`, `.tsx`, `.js`, `.jsx`). The CLI builds it into a standalone ES module. Required for Worker UI and headless targets. |

### Action fields

Apply to `*.details.action`, `*.index.action` and `*.index.selection-action` of every resource.

| Field | Type | Required | Description |
|---|---|---|---|
| `label` | string | Yes | Menu item label, up to 40 characters. |
| `presentation` | string | Yes | `headless`, `modal`, `fullscreen` or `navigate`. See [action types](/developers/apps/en/targets#action-types). |
| `title` | string | No | Modal title (`modal`, `fullscreen`). Defaults to `label`. |
| `page` | string | With `navigate` | `id` of the `app.page` extension to open. |
| `path` | string | No | Path inside the page for `navigate`, for example `/returns`. |
| `conditions` | object | No | When to show the menu item, evaluated by POS. See [conditions](#conditions). |
| `should_render` | string | No | A module returning `true`/`false` to decide whether to show the item. Use it when `conditions` is not enough. |

### Block fields

Apply to `*.details.block`.

| Field | Type | Required | Description |
|---|---|---|---|
| `conditions` | object | No | When to show the block. |
| `should_render` | string | No | Same as for actions. |

### Page and menu fields

| Target | Field | Description |
|---|---|---|
| `app.page` | `title` (required) | Page title. |
| `app.page` | `render` (required) | `iframe` or `worker` (Worker UI pages are planned for phase 2). |
| `app.page` | `entry` | The page's HTML file in the bundle, with `render: "iframe"`. Declare `entry` **or** `url`. |
| `app.page` | `url` | Self-hosted page: an `https` address on your own server that POS embeds instead of a file in the bundle. No credentials, query or `#`; no Pancake domains, `localhost` or IP addresses. Changing `url` requires a new version. See [Self-hosted pages](/developers/apps/en/runtime-api#self-hosted-pages). |
| `app.page` | `module` | Required when `render: "worker"`. |
| `nav.item` | `label` (required) | Label in the POS menu, up to 30 characters. |
| `nav.item` | `page` (required) | `id` of the `app.page` extension. |

### Hook fields

Apply to `*.before_submit` and `*.after_save`.

| Field | Type | Default | Description |
|---|---|---|---|
| `timeout_ms` | int | `2000` | `before_submit` only, at most `5000`. |
| `on_error` | string | `allow` | `before_submit` only: `allow` lets the save continue when the hook fails or times out, `block` stops it. |

## conditions

Keys are combined with **AND**; values in an array within one key are combined with **OR**. POS evaluates conditions while rendering, so menus never slow down.

| Key | Type | Resources | True when |
|---|---|---|---|
| `mode` | string[] | All (detail form targets) | `create` (new, unsaved record) or `edit` (existing record). |
| `status` | int[] | `order`, `purchase`, `export`, `warehouse_transfer` | The record status is in the list (status codes as in the Open API). |
| `warehouse_ids` | uuid[] | `order`, `purchase`, `export`, `warehouse_transfer` | The record's warehouse is in the list; for a transfer, either the source or the destination warehouse. |
| `tags_any` | (int \| string)[] | `order`, `product`, `purchase`, `export` | The record has at least one of the listed tags (tag IDs). |
| `type` | int[] | `export` | The export type is in the list. |
| `category_ids` | int[] | `product` | The product belongs to at least one of the listed categories. |
| `order_sources` | string[] | `order` | The order source is in the list. |
| `has_customer` | boolean | `order` | The order has / does not have a customer attached. |

Using a key that does not apply to the target's resource is a validation error (`invalid_condition`).

Example: only show for existing orders in status 1 or 2 that carry the VIP tag (ID 7):

```json
{ "mode": ["edit"], "status": [1, 2], "tags_any": [7] }
```

Example: only show for purchases that have been received (status 1) into one specific warehouse:

```json
{ "status": [1], "warehouse_ids": ["a52f…"] }
```

## Manifest validation errors

| Code | Cause |
|---|---|
| `invalid_handle` | `handle` has an invalid format. |
| `invalid_field` | A field has an invalid value, for example it is too long, `access` has an unknown key, or `access.methods` is invalid. |
| `duplicate_version` | `version` already exists. |
| `unsupported_api_version` | `api_version` is no longer supported or does not exist. |
| `unknown_target` | `target` does not exist in the declared `api_version`. |
| `missing_field` | A field required by the target is missing. |
| `module_not_found` | The `module`/`entry`/`should_render` file cannot be found. |
| `bundle_too_large` | A module or the bundle exceeds the size limits. |
| `storage_quota_exceeded` | The shop's total app storage would exceed 500 MB. |
| `invalid_bundle` | The uploaded file is not a valid `.zip` package, or contains an unsafe file path. |
| `upload_failed` | The bundle could not be stored; upload again. |
| `invalid_origin` | An `allowed_origins` entry is not `https://` or uses a wildcard. |
| `invalid_url` | The `app.page` `url` is not `https` with a domain name, contains credentials, a query or `#`, or belongs to a Pancake domain. |
| `invalid_condition` | A `conditions` key does not apply to the target's resource. |
