App API
Routes, permissions, pagination, idempotency and errors of the App API, and the PolturaClient methods that call them.
On this page
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), and can reach only what the
merchant granted to that installation. New to it? Start with Make your first API 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.
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
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
| 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
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. 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
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, generated from the same contracts as the SDK.
Next
- Make your first API call: how your server gets a token, and a paginated product list.
- Authentication: tokens, refresh and permissions.