Inventory
Stock-managed SALE items use variant rows, or an item-level row when the
item has never had a variant. Every inventory row exposes three quantities
plus a derived status. Examples reuse the
api() helper.
| Field | Meaning |
|---|---|
totalQuantity | Physical units in stock, including reserved units |
reservedQuantity | Units held (e.g. for pending orders) |
availableQuantity | totalQuantity − reservedQuantity — units available to sell |
stockStatus | IN_STOCK · LOW_STOCK · OUT_OF_STOCK (derived) |
lowStockThreshold | Positive availableQuantity at/below this threshold is LOW_STOCK; 0 disables the low-stock alarm |
availableQuantity <= 0 produces OUT_OF_STOCK, including when the
low-stock alarm is disabled.
Read stock
// Public — a storefront checking availability for a PDP
const rows = await api<Inventory[]>(`/public/inventory/${itemId}`);
// Merchant — filterable, enriched with item name/slug
const merchantRows = await api<Inventory[]>(
"/merchant/inventory?stockStatus=LOW_STOCK", { token },
);
// Merchant — counts by status for a dashboard
const summary = await api("/merchant/inventory/summary", { token });
// → { inStock, lowStock, outOfStock, total }Both stock reads return bare arrays, not a { rows } envelope. The
summary returns counts of inventory rows in each status, not unit quantities.
Merchant rows include item and variant summaries; public rows include the
item summary and variantId, but omit the merchant-only variant summary.
Public reads return only eligible rows for active, non-archived items;
composed bundle parents are excluded. RENTAL and SERVICE items normally
have no stock rows, and a current SALE variant can also lack one. An empty
array does not establish that no rows exist or that the item is out of stock.
Both public list and per-item reads have a 5,000-row cap, with no cursor,
total or truncation marker; the per-item read does not bypass that cap. See
the API reference for the full visibility contract.
Mutations
All mutations are POST /merchant/inventory/:id/<action> where :id is the
inventory row id. Mutations require inventory:adjust; Owner/Admin retain
broad access, and Staff needs that group.
// Adjust on-hand by a signed delta (receiving stock, shrinkage, corrections)
await api(`/merchant/inventory/${id}/adjust`, { method: "POST", token,
body: JSON.stringify({ delta: 24, reason: "Received PO #1182" }) });
// Hold units (availableQuantity → reservedQuantity).
await api(`/merchant/inventory/${id}/reserve`, { method: "POST", token,
body: JSON.stringify({ quantity: 2, reason: "manual hold" }) });
// Release unprotected units (reservedQuantity → availableQuantity).
await api(`/merchant/inventory/${id}/release`, { method: "POST", token,
body: JSON.stringify({ quantity: 2 }) });
// Low-stock alarm. 0 disables it. Recomputes stockStatus immediately.
await api(`/merchant/inventory/${id}/threshold`, { method: "POST", token,
body: JSON.stringify({ lowStockThreshold: 5 }) });adjust requires a non-zero integer delta and rejects a change that would
make totalQuantity or availableQuantity negative. Reserve and release
take positive integer quantities and leave totalQuantity unchanged.
Reserve rejects a quantity greater than availableQuantity; release rejects
one greater than reservedQuantity and returns 409 RESOURCE_CONFLICT if
the remaining reservation would violate a live booking/POS reservation floor.
Release can also return that conflict when the stored reservation is already
below its protected floor. Use the owning booking/POS lifecycle to release
its reservation.
reason is required on adjust (1–200 chars) and optional on reserve/release
(up to 200 chars). It is written into the audit log so stock
movements are traceable.
Related
- Catalog: products & variants — what stock attaches to
- Audit log — every adjustment is recorded