Stock

Stock on revenexx — locations, stock levels, the append-only movements ledger, the reserve/commit/release lifecycle, reorder alerts, and availability as the designated override point.

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

Request
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.

This is the designated override point. A tenant whose stock really lives in an ERP replaces exactly this one capability with their own app while every other route here keeps doing the stock-keeping unchanged. That is why the request and response read as a contract to be implemented: whatever ends up answering this path has to answer in these terms. See From manifest to installed.

Locations

Request
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:

  • priority decides where a reservation is served from. Nothing branches on type.
  • enabled defaults to true, 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.

Request
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

RouteBooksNote
POST /v1/inventories/receive+on_hand, inboundGoods in. Answers 201. A reason is optional unless movement_reason_required is all.
POST /v1/inventories/adjust±on_hand, adjustmentBatch 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, adjustmentThe 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_handReturns back to stock. Answers 200, because it may legitimately create nothing.
Request
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

Request
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:

StrategyPicks
priorityWalks the enabled locations by their priority.
nearestMatches ship_to against a location's country.
single_locationOne 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.

Request
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.

There is no partial commit and no per-line id. Commit takes the whole 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

Request
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

Request
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

text
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.

Where to go next

  • Orders — the ship, cancel and complete side.
  • Returns — where the restock array comes from.
  • Cartsconvert_reserves_stock.
  • Settings — where allocation_strategy and friends live.
Was this page helpful?