Inventory
Stock objects, availability, movements, reservations, warehouses and SKU aliases — the project-scoped stock engine behind orders.
The inventory module tracks physical stock for a project (one or more stores sharing
store.projectId). Everything lives under /v1/inventory; all endpoints require a bearer token
except the public storefront availability check.
- Permissions. The store owner can do everything. Members need
inventory.readfor lists andinventory.writefor mutations. The inventory module must be enabled for the store (billing entitlement — the public endpoint returns402when it isn't). - Lists accept
page,limit,searchand the filterator; batch endpoints return the partial-success envelope.
Concepts
Inventory objects are stock buckets keyed by (namespace, sku, batch) inside a project.
Each object tracks quantityOnHand and quantityReserved (available = on hand − reserved),
plus a storageClass (HOT, WARM, COLD, QUARANTINE), optional warehouseId, priority,
expiresAt and free-form metadata (receipt stock-in stores unit_cost there).
Products map to inventory by scoped SKU. The product's sku is the root scope; variant
options and addons may carry their own SKUs, conventionally nested as paths:
| SKU | Meaning |
|---|---|
tshirt | Product root scope |
tshirt/red/m | Variant path (option names along variantPath) |
tshirt/addon/gift-wrap | Product addon |
When an order line resolves its SKU, the leaf variant option wins, then parent options, then the
product root. Availability lookups accept a trailing * wildcard — tshirt* aggregates the
whole scope subtree, tshirt matches only the exact SKU. Aliases map external codes
(supplier refs, barcodes) onto a target SKU.
How orders touch inventory
Orders drive reservations automatically via the order-inventory bridge. Defaults per store
(overridable in store.configs.inventory_integration):
| Hook | Default order statuses | Effect |
|---|---|---|
reserve_on | pending, review, accepted, followup, processing | Creates a reservation (holderRef = order id) and bumps quantityReserved; records reason: "reserve" movements |
unreserve_on | cancelled, draft | Releases the hold; records reason: "release" movements |
consume_on | completed | Converts the hold into a stock decrement (consume) |
Additional knobs: missing_bucket_policy (ignore skips untracked SKUs — the default — while
reject fails the order), allow_backorder (default true: reservations may exceed on-hand,
availability goes negative), and lifecycle *_rules for conditional matching. Every stock change
— receive, reserve, release, consume, adjustment, receipt void — is an immutable movement
with delta, reason, correlationRef and balanceAfter, so GET /v1/inventory/movements is
the full audit trail. Order events themselves are also pushed via webhooks.
At a glance
Projects & public availability
| Method | Path | Description | Auth |
|---|---|---|---|
POST | /v1/inventory/public/availability | Storefront availability: { storeId, skus } → map of SKU to quantity (max 250 SKUs, cached ~15 s) | Public |
GET | /v1/inventory/projects | List projects linked to your stores | Bearer |
POST | /v1/inventory/projects | Create a project | Bearer |
Objects
| Method | Path | Description | Auth |
|---|---|---|---|
GET | /v1/inventory/objects | List stock buckets (namespace, sku, storageClass, search filters) | Bearer |
POST | /v1/inventory/objects | Receive stock into a bucket (creates or tops up) | Bearer |
GET | /v1/inventory/objects/{id} | Get one | Bearer |
PUT | /v1/inventory/objects/{id} | Update namespace/batch/priority/storageClass/warehouse/expiresAt/metadata | Bearer |
DELETE | /v1/inventory/objects/{id} | Delete — fails if the bucket has active reservations | Bearer |
POST | /v1/inventory/objects/apply-deltas | Adjust quantities: { deltas: [{ objectId, quantityDelta }], reason } | Bearer |
POST | /v1/inventory/objects:batchDelete | Delete many | Bearer |
POST | /v1/inventory/objects:batchUpdate | Update many with updateMask + shared fields | Bearer |
Availability & movements
| Method | Path | Description | Auth |
|---|---|---|---|
GET | /v1/inventory/availability | skus (comma-separated, * wildcard) → available quantities; pass namespace for per-namespace breakdown | Bearer |
GET | /v1/inventory/movements | Audit trail (objectId, correlationRef, reason filters) | Bearer |
Reservations
| Method | Path | Description | Auth |
|---|---|---|---|
GET | /v1/inventory/reservations | List holds (state, holderRef filters) | Bearer |
GET | /v1/inventory/reservations/{id} | Get one with lines + objects | Bearer |
GET | /v1/inventory/reservations/holder/{holderRef} | Holds for a holder (e.g. an order id) | Bearer |
POST | /v1/inventory/reservations/{id}:release | Release a hold (projectId query param required) | Bearer |
POST | /v1/inventory/reservations:batchRelease | Release many | Bearer |
Reservations are created by the system (order flow or receipt posting) — there is no create endpoint. Release is the only mutation.
Aliases & warehouses
| Method | Path | Description | Auth |
|---|---|---|---|
GET | /v1/inventory/aliases | List SKU aliases (targetSku, search filters) | Bearer |
POST | /v1/inventory/aliases | Create { alias, targetSku } | Bearer |
PUT | /v1/inventory/aliases/{alias} | Repoint an alias (projectId query param) | Bearer |
DELETE | /v1/inventory/aliases/{alias} | Delete (projectId query param) | Bearer |
POST | /v1/inventory/aliases:batchDelete | Delete many (names are alias strings) | Bearer |
GET | /v1/inventory/warehouses | List warehouses | Bearer |
POST | /v1/inventory/warehouses | Create (name, code, optional namespacePrefix) | Bearer |
PUT | /v1/inventory/warehouses/{id} | Update | Bearer |
DELETE | /v1/inventory/warehouses/{id} | Delete | Bearer |
POST | /v1/inventory/warehouses:batchDelete | Delete many | Bearer |
Reset
| Method | Path | Description | Auth |
|---|---|---|---|
POST | /v1/inventory/reset | Wipe project inventory data — owner only, body confirm: "RESET" | Bearer |
Examples
Receive stock into a batch bucket:
curl -X POST "https://api.feeef.org/v1/inventory/objects" \
-H "Authorization: Bearer $FEEEF_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"projectId": "PROJECT_ID",
"sku": "tshirt/red/m",
"batch": "B-2026-08",
"quantity": 100
}'Check availability for a SKU scope (wildcard aggregates variants):
curl -H "Authorization: Bearer $FEEEF_TOKEN" \
"https://api.feeef.org/v1/inventory/availability?projectId=PROJECT_ID&skus=tshirt*,mug"Adjust quantities (stock count correction) — on hand can never go negative or below reserved:
curl -X POST "https://api.feeef.org/v1/inventory/objects/apply-deltas" \
-H "Authorization: Bearer $FEEEF_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"projectId": "PROJECT_ID",
"deltas": [{ "objectId": "OBJECT_ID", "quantityDelta": -3 }],
"reason": "damaged"
}'Inspect an order's hold and release it manually:
curl -H "Authorization: Bearer $FEEEF_TOKEN" \
"https://api.feeef.org/v1/inventory/reservations/holder/ORDER_ID?projectId=PROJECT_ID"
curl -X POST -H "Authorization: Bearer $FEEEF_TOKEN" \
"https://api.feeef.org/v1/inventory/reservations/RESERVATION_ID:release?projectId=PROJECT_ID"Storefront availability (no token — safe to call from the browser):
curl -X POST "https://api.feeef.org/v1/inventory/public/availability" \
-H "Content-Type: application/json" \
-d '{ "storeId": "STORE_ID", "skus": ["tshirt*", "mug"] }'Notes
- Receiving via
POST /v1/inventory/objectstops up an existing(namespace, sku, batch)bucket instead of duplicating it. Purchase receipts are the finance-side way to stock in — they also enforce a per-batch unit cost. - Batch updates use
updateMaskto whitelist fields:{ projectId, names, updateMask: ["storageClass"], storageClass: "QUARANTINE" }. - Availability responses are cached (~15 s) and invalidated on stock mutations; the public endpoint additionally caps requests at 250 SKUs.
- Movements and reservations are immutable audit records — there are no update or delete endpoints for them.