Overview
A ledger-backed stock engine — objects, balanced operations, and holder-keyed reservations.
Scope: inventory:read for objects, availability, movements, namespaces, and
reservation state; inventory:write for operations and rules;
inventory:reserve for hold / consume / release. Base path: /inventory.
The model
Stock lives in objects, keyed by (namespace, key, batch). Namespace is
the location (wh-main, a zone, or a virtual area like @production). Key
is a hierarchical SKU path (tshirt/Red/XL). Batch defaults to '' and
stays invisible until you turn batches on.
There is no warehouse registry — a namespace is just a string. Enumerate
them with GET /inventory/namespaces/browse.
Each object stores two integer counters: quantityOnHand and
quantityReserved. Available is always derived
(onHand − reserved) — never stored, never patched.
On-hand changes only through operations (receive, transfer, convert,
fulfill, …). Reserved changes only through reservations (reserve,
consume, release, expire). Every change appends a movement with
balanceAfter, so a client never has to reconstruct history.
Three things that trip people up
You cannot PATCH a quantity. PUT /objects/batch updates metadata
(priority, unitCost). Raising or lowering stock is always an operation,
which is why every adjustment carries a type and (for adjust) a reason. If
an integration writes counters directly it is talking to the wrong API.
A transfer that does not net to zero is a 422 unbalanced_operation.
The engine will not silently create or destroy goods when you move them
between namespaces. Same key, same batch, opposite signs, same absolute
quantity.
Reservations are idempotent by holder, not by request body. Re-posting
the same holderRef with the same lines is a no-op (200). Re-posting
different lines releases the old hold and takes the new one. Retries are
safe; "edit the cart" is the same endpoint.
Where to go
Objects — keys, availability, browse, rules. Operations — the balanced write path. Reservations — hold, consume, release.