# App API (/api/app)

The App API is the HTTP API your server uses to read and change a store's data, under `/apps-api/v1`. Every
request carries an installation access token from the merchant's grant (see [Authentication](/apps/auth#installing)), and can reach only what the
merchant granted to that installation. New to it? Start with [Make your first API call](/api/app/first-call).

The production origin of the App API is not published yet, so the examples read it from `POLTURA_API`.

`PolturaClient` from `@poltura/app-sdk` wraps every route:
`new PolturaClient({ origin, accessToken, timeoutMs?, maximumResponseBytes?, fetch? })`. `origin` is an HTTPS
origin with no path. Requests time out after 10 seconds unless you set `timeoutMs` (up to 60 seconds), and
answers over 1 MiB are refused unless you raise `maximumResponseBytes` (up to 8 MiB). The client sends the token
as `Authorization: Bearer`, never sends cookies and never follows redirects.

A write, from the complete example: saving to app storage with the version it replaces and an idempotency key.



```ts
export async function saveFeatured(
  client: PolturaClient,
  productIds: readonly string[],
  expectedVersion: number,
  idempotencyKey: string,
): Promise<StorageRecord> {
  const value = FeaturedProducts.parse({ productIds: [...productIds] });
  return client.storage.put(STORAGE_NAMESPACE, FEATURED_KEY, value, {
    expectedVersion,
    idempotencyKey,
  });
}
```

## Routes [#routes]

Lists take `limit` (1 to 100, default 25) and `cursor`, and answer with `nextCursor`: pass it back to get the
next page, until it is `null`. Each route also has its own page in this section, with parameters and examples.

| Method | Path                                     | Permission          | SDK                     | Request                                                                                         | Response                                                                                                    |
| ------ | ---------------------------------------- | ------------------- | ----------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| GET    | `/apps-api/v1/store`                     | `settings.read`     | `store.retrieve`        |                                                                                                 | `StoreContext` `{ id, name, locale, currency, timezone }`                                                   |
| GET    | `/apps-api/v1/installation`              | none                | `installation.retrieve` | no tenant override                                                                              | `InstallationContext` `{ installationId, storeId, appId, releaseId, generation, permissions }` (`permissions` max 32) |
| GET    | `/apps-api/v1/products`                  | `products.read`     | `products.list`         | `ListOptions`                                                                                   | `ProductPage` `{ products, nextCursor }` (max 100)                                                          |
| GET    | `/apps-api/v1/products/{id}`             | `products.read`     | `products.retrieve`     | `ProductId`                                                                                     | `ProductResponse` `{ product }`                                                                             |
| PATCH  | `/apps-api/v1/products/{id}`             | `products.write`    | `products.update`       | `ProductWriteInput` `{ title?, description?, handle?, status?, metadata? }` + `Idempotency-Key` | `WriteResult` `{ ok: true, version }`                                                                       |
| GET    | `/apps-api/v1/collections`               | `products.read`     | `collections.list`      | `ListOptions`                                                                                   | `CollectionPage`                                                                                            |
| GET    | `/apps-api/v1/collections/{id}`          | `products.read`     | `collections.retrieve`  | `CollectionId`                                                                                  | `CollectionResponse`                                                                                        |
| PATCH  | `/apps-api/v1/collections/{id}`          | `products.write`    | `collections.update`    | `CollectionWriteInput` `{ title?, handle?, metadata? }` + `Idempotency-Key`                     | `WriteResult`                                                                                               |
| GET    | `/apps-api/v1/orders`                    | `orders.read`       | `orders.list`           | `OrderListOptions` (`status?`, `updatedAfter?`)                                                 | `OrderPage`                                                                                                 |
| GET    | `/apps-api/v1/orders/{id}`               | `orders.read`       | `orders.retrieve`       | `OrderId`                                                                                       | `OrderResponse` `{ order: PublicOrder }`                                                                    |
| PATCH  | `/apps-api/v1/orders/{id}`               | `orders.write`      | `orders.update`         | `OrderWriteInput` `{ metadata?, tags?, note? }` + `Idempotency-Key`                             | `WriteResult`                                                                                               |
| GET    | `/apps-api/v1/customers`                 | `customers.read`    | `customers.list`        | `CustomerListOptions` (`email?`, `updatedAfter?`)                                               | `CustomerPage`                                                                                              |
| GET    | `/apps-api/v1/customers/{id}`            | `customers.read`    | `customers.retrieve`    | `CustomerId`                                                                                    | `CustomerResponse`                                                                                          |
| PATCH  | `/apps-api/v1/customers/{id}`            | `customers.write`   | `customers.update`      | `CustomerWriteInput` `{ metadata?, tags? }` + `Idempotency-Key`                                 | `WriteResult`                                                                                               |
| GET    | `/apps-api/v1/inventory`                 | `products.read`     | `inventory.list`        | `InventoryListOptions` (`variantId?`, `sku?`)                                                   | `InventoryPage` `{ levels, nextCursor }`                                                                    |
| GET    | `/apps-api/v1/storage/{namespace}`       | none                | `storage.list`          | `StorageListQuery`                                                                              | `StoragePage`                                                                                               |
| GET    | `/apps-api/v1/storage/{namespace}/{key}` | none                | `storage.get`           |                                                                                                 | `StorageRecord`                                                                                             |
| PUT    | `/apps-api/v1/storage/{namespace}/{key}` | none                | `storage.put`           | `StoragePutInput` `{ value, expectedVersion }` + `Idempotency-Key`                              | `StorageRecord`                                                                                             |
| DELETE | `/apps-api/v1/storage/{namespace}/{key}` | none                | `storage.delete`        | `StorageDeleteInput` `{ expectedVersion }` + `Idempotency-Key`                                  | `StorageDeleteResult`                                                                                       |

`CatalogueProduct`: `{ id, title` 1–500, `handle` 1–255, `description` max 20000 or null, `image`, `updatedAt }`. `PublicProductWriteStatus` is `draft` \| `published`. `PublicMetadata` max 64 snake_case keys, values max 2000. `PublicTags` max 32. `PublicOrder` includes `orderNumber` (1–64), `items` max 200 and `shippingAddress`. Money is `{ amount: "49.99", currency: "EUR" }` in major units; do not multiply by 100.

A write counts as two requests against the installation's quota. Writing a field that the route does not allow
fails with `invalid_input`. Every write takes an `Idempotency-Key` header, so a retried request is applied once.

## Storage limits [#storage-limits]

| Rule                             | Value             |
| -------------------------------- | ----------------- |
| Value bytes                      | 65536             |
| JSON depth                       | 8                 |
| JSON nodes                       | 1024              |
| Namespaces                       | 16                |
| Bytes / records per installation | 10 MiB / 1000     |
| List page                        | 100 / 900 KiB     |
| Live idempotency                 | 128               |
| Idempotency TTL                  | 24 h              |
| Cursor TTL                       | 1 h               |
| First put                        | creates version 1 with `expectedVersion` 0 |

A namespace is 1 to 64 characters, lowercase letters, digits and `_`, starting with a letter. A key is 1 to 128
characters, letters, digits, `.`, `_` and `-`, starting with a letter or digit. Sending the same idempotency key
with the same body returns the first answer again; with a different body it fails with `conflict`. A write whose
`expectedVersion` is not the current version fails with `stale_version`: read the record again and retry.

## Backend actions [#backend-actions]

Actions go the other way: a worker calls `useAction(actionId)`, and Poltura sends a signed `POST` with
`{ input, context }` to your `backendOrigin`. They are not part of the App API; see
[Signed actions](/apps/auth#signed-actions). Limits per installation: 60 calls a minute from the admin and 300
from the storefront, and 10 a minute per shopper. When a call cannot complete, the worker gets one of
`not_declared`, `timeout`, `upstream`, `too_large` or `unavailable`.

## Errors [#errors]

A failed call throws `PolturaRequestError` with `code`, `status`, `requestId` (quote it when you ask for help)
and, for `rate_limited`, `retryAfterMs`. When the request never got an answer the code is one of
`response_too_large`, `invalid_response`, `request_failed`, `aborted` or `unavailable`.

The API's error body is `{ "error": { "code", "message", "requestId", "details"?, "retryAfterMs"? } }`, with these
codes:

| `code`                    | HTTP |
| ------------------------- | ---- |
| `invalid_input`           | 400  |
| `unauthorized`            | 401  |
| `forbidden`               | 403  |
| `not_found`               | 404  |
| `conflict`                | 409  |
| `stale_version`           | 412  |
| `quota_exceeded`          | 409  |
| `rate_limited`            | 429  |
| `payload_too_large`       | 413  |
| `unsupported_api_version` | 400  |
| `unavailable`             | 503  |

The OpenAPI 3.1 specification is at [/openapi.json](/openapi.json), generated from the same contracts as the SDK.

## Next [#next]

- [Make your first API call](/api/app/first-call): how your server gets a token, and a paginated product list.
- [Authentication](/apps/auth): tokens, refresh and permissions.
