Stock
Base path: https://api.revenexx.com/v1/inventories. Four entities: locations, stock_levels, stock_movements, reservations.
The domain rule
Every stock change is a movement. on_hand and reserved are never written by an edit — they are the running total of the movements ledger, and only the booking routes move them. A PUT on a stock row that sends on_hand has those keys dropped.
That is why there is no editable quantity field anywhere in this app. A correction is another booking, so a number can never silently differ from yesterday's.
Availability — the call to know
curl -X POST https://api.revenexx.com/v1/inventories/availability \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"items":[{"sku":"ACME-4711-BLK","quantity":5},{"sku":"ACME-9001","quantity":2}]}'
A batch call. Each item comes back with on_hand, reserved, the derived available (their difference, computed on read and stored nowhere), a per-location breakdown, and orderable — whether that much can be promised at this moment.
An item this app has never seen is not an error: it answers tracked: false, and the storefront decides whether an untracked item sells freely. At most 200 items per call; naming none is a 400.
Locations
curl -X POST https://api.revenexx.com/v1/inventories/locations \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"code":"leipzig","name":"Leipzig warehouse","type":"warehouse","priority":20}'
type is warehouse, store (a shop floor a click-and-collect order draws on), dropship (a supplier whose stock this row only tracks) or virtual (pre-orders, consignment, a quarantine shelf).
code and name are required, and code is unique per tenant — the 409 answers both a second insert and an update that moves a row onto a sibling's value.
Two columns are not decoration:
prioritydecides where a reservation is served from. Nothing branches ontype.enableddefaults totrue, so a location created for a warehouse that has not opened yet starts being offered by availability and reserve immediately.
A new location starts empty, and creating one moves nothing. Stock arrives through a receipt, or is transferred by two adjustments — one negative at the old location, one positive here.
POST /v1/inventories/locations/defaults seeds the main warehouse.
Stock levels
A stock row is an item at a location: location_id plus product_id and/or sku, its reorder_point and its metadata.
curl "https://api.revenexx.com/v1/inventories/stock?limit=50" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
You mostly do not need POST /v1/inventories/stock at all — every stock call creates the row it is missing. The row is born at zero and on_hand/reserved are not accepted, so an opening balance is a receipt, not a field.
A second row for an item a location already tracks answers 409: no unique index enforces one row per item per location, so that row would split the item's balance across two rows the write routes cannot tell apart.
The booking routes
| Route | Books | Note |
|---|---|---|
POST /v1/inventories/receive | +on_hand, inbound | Goods in. Answers 201. A reason is optional unless movement_reason_required is all. |
POST /v1/inventories/adjust | ±on_hand, adjustment | Batch correction — stocktake, breakage, shrinkage. Quantities are signed. A reason is mandatory unless movement_reason_required is none. |
POST /v1/inventories/stock/{id}/adjust | ±on_hand, adjustment | The row-scoped twin: the row already knows its location and item, so you owe a signed delta and a reason. |
POST /v1/inventories/restock | +on_hand | Returns back to stock. Answers 200, because it may legitimately create nothing. |
curl -X POST https://api.revenexx.com/v1/inventories/receive \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"sku":"ACME-4711-BLK","quantity":120,"location_code":"leipzig","reason":"DN 88213"}'
The receiving location is the caller's location_code, else the default_location_code setting.
Whether a return rejoins sellable stock follows restock_on_return_default, overridable per call with restock. When the answer is no, the response says restocked: false and nothing moves — there is no movement to book, because no stock moved.
Reservations: reserve → commit | release
curl -X POST https://api.revenexx.com/v1/inventories/reserve \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"order_ref":"ORD-000123","items":[{"sku":"ACME-4711-BLK","quantity":6}]}'
A hold against an order_ref. The whole call is planned before anything is written, so a reservation that cannot be satisfied changes nothing.
Which location serves an item is not yours to choose. The tenant's allocation_strategy decides:
| Strategy | Picks |
|---|---|
priority | Walks the enabled locations by their priority. |
nearest | Matches ship_to against a location's country. |
single_location | One location for the whole order. |
backorder_policy decides what happens when none can serve: refuse with 422, or reserve anyway and let availability go negative. expires_at defaults from reservation_ttl_minutes.
Reserving raises reserved and does not move on_hand — the stock is still on the shelf.
curl -X POST https://api.revenexx.com/v1/inventories/commit \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"order_ref":"ORD-000123"}'
Commit when the goods leave the building, and not before. It lowers both numbers on each row and writes one shipment booking per hold, with a signed negative quantity.
order_ref and every hold still active on it — so a part shipment means reserving the parts separately in the first place.committed is final. Goods coming back are a restock (a new receipt), never an undo of a commit.POST /v1/inventories/release is the cancellation end: it ends every hold still active on an order_ref, gives the stock back and writes a release booking for each.
The two answer an empty order differently, on purpose. release is idempotent and answers 200 with released: 0 — cancelling twice is harmless. commit answers 422 — it means the hold was already released or already shipped, and shipping twice is not harmless.
POST /v1/inventories/reservations/sweep is the expiry sweeper, also run on a schedule every 15 minutes. It releases reservations past their expires_at and — once reservation_ttl_minutes is above 0 — reservations older than that lifetime which never carried a deadline. Idempotent: a second run finds nothing.
The movements ledger
curl "https://api.revenexx.com/v1/inventories/movements?order=created_at.desc&limit=50" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
Every stock change this app has ever made is a booking row: a receipt, a correction, a hold, a release, a shipment, a return. That is what lets one list be an audit trail and an event feed at the same time — these are the rows the stock_movement.created event carries, so a consumer that missed an event catches up by paging here.
Append-only. No update, no delete: a correction is another booking. order=created_at.desc is the feed order.
Reorder alerts
curl https://api.revenexx.com/v1/inventories/reorder-alerts \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
The replenishment worklist, computed on read so it is never stale. A row alerts when available (on_hand − reserved) has fallen to or below its own reorder_point, or the reorder_point_default setting when it carries none. A point of 0 never alerts.
It answers enabled: false with an empty list when reorder_alert_enabled is off — a tenant replenishing from an ERP should not be told twice.
Where it fits in an order
POST /v1/carts/{id}/order ─ may reserve, per convert_reserves_stock
POST /v1/inventories/reserve ─ hold the stock
POST /v1/orders/{id}/ship ─ book the goods out on the order
POST /v1/inventories/commit ─ consume the hold: −on_hand −reserved
POST /v1/inventories/release ─ instead, if the order is cancelled
POST /v1/inventories/restock ─ after a completed return
None of those calls each other. The orchestration is yours.