The inventory engine: one quantity the order can trust
How Feeef stock works now — scoped SKUs, reservations, movements, and why the number on the product is no longer the stock.
A COD store learns this the hard way. Two people confirm the same variant in the same minute, both see "3 left", and the warehouse ships one of them. The number on the product card was a field. It was not a lock.
The inventory module is the lock. Quantities live in the Cloud inventory engine. The API you call is still /v1/inventory. Feeef accepts the request; on-hand and reserved counts come from Cloud. The full reference is the inventory guide. This post is the model.
A bucket, not a product field
Stock is an object: one bucket keyed by namespace, SKU, and batch, inside a project. A project is the thing one or more stores share (store.projectId). The bucket stores quantityOnHand and quantityReserved. Available is on hand minus reserved.
Products point at those buckets by SKU. The product's own sku is the root. A variant hangs off it as a path (tshirt/red/m). An addon can too (tshirt/addon/gift-wrap). When an order line needs a SKU, the leaf variant wins, then parent options, then the product root.
Availability accepts a trailing *. tshirt* is the whole subtree. tshirt is that exact bucket. Aliases map a barcode or a supplier code onto the SKU you actually stock.
Once the module is on, stop treating product.stock as the truth. Bind the variant to a scoped SKU and read availability. The public check is POST /v1/inventory/public/availability with { storeId, skus } — up to 250 SKUs, cached for about fifteen seconds. It returns 402 when the store is not entitled to inventory.
What an order does to stock
Orders do not decrement a counter in the controller and hope. They go through a reservation.
| Moment | Default statuses | What happens |
|---|---|---|
| Reserve | pending, review, accepted, followup, processing | A hold, holderRef = the order id. quantityReserved goes up. |
| Release | cancelled, draft | The hold comes off. |
| Consume | completed | The hold becomes a real decrement. |
You can change those status lists per store in store.configs.inventory_integration. Two knobs matter on day one:
missing_bucket_policy—ignore(default) skips a SKU you do not track.rejectfails the order.allow_backorder— defaulttrue, so a reservation may exceed on-hand and availability goes negative. Turn it off when you would rather refuse the order.
Every change is a movement: delta, reason (reserve, release, consume, receive, an adjustment), correlationRef, balanceAfter. GET /v1/inventory/movements is the audit log. You do not reconstruct history by diffing a stock column.
Receiving stock is a finance action
Buying goods is not "type 50 into the product". A purchase receipt, posted from finance, does three things in one transaction: the lines become inventory objects (the batch keeps unit_cost in metadata), each line writes a receive movement, and an open supplier bill is created.
An existing batch only accepts more stock at the same unit cost. A different cost is BATCH_COST_MISMATCH — use another batch. Voiding a posted receipt reverses the stock-in, and it refuses if that quantity was already reserved or sold.
Warehouses are still how the merchant app groups stock. A transfer is POST /v1/inventory/operations with type transfer. Adjustments and receives use the same operations route. Direct quantity edits go through POST /v1/inventory/objects/apply-deltas.
What to wire first
- Create or reuse the project (
GET /v1/inventory/projects). - Receive a bucket for one real SKU, with a batch and a unit cost.
- Place a test order in a reserving status and read the reservation.
- Complete it and read the movement. The available quantity should have dropped once, not twice.
- Point the storefront at
POST /v1/inventory/public/availabilityinstead of the catalog stock field.
If those five steps match what the warehouse did, the engine is doing its job. The rest — aliases, storage class, expiry, barcode labels — hangs off the same bucket.
Give every block a browser budget
Why Helium keeps custom components interactive by default, and how theme authors can remove browser work with confidence.
Sending 500 orders
The courier takes 50 or 100 parcels per call. A desk selection of 500 is one Feeef request, then chunks with a short rest between them.