Skip to content

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.

FieldMeaning
totalQuantityPhysical units in stock, including reserved units
reservedQuantityUnits held (e.g. for pending orders)
availableQuantitytotalQuantity − reservedQuantity — units available to sell
stockStatusIN_STOCK · LOW_STOCK · OUT_OF_STOCK (derived)
lowStockThresholdPositive 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.