Manage stock across warehouses

Keep one item's stock in several locations, choose which location a reservation draws from, read availability per location and commit a shipment, and know what happens when no single location holds the whole quantity.

A tenant with more than one warehouse has one question for every order: which location does this stock come from? Inventories answers it when you reserve, by a rule the merchant picks, and it answers per item: the platform never splits one item across locations, though you can (step 4). This guide sets up the locations, shows what each rule does, reads availability per location, and ends with the commit that ships the goods out of the right shelves.

It is for developers and integrators wiring a storefront, an ERP sync or a warehouse tool to the Inventories app. You need a tenant with the Inventories app, an API key that covers it (inventories.*, or single operations: inventories.locations.create, inventories.locations.update, inventories.locations.list, inventories.receive, inventories.availability, inventories.reserve, inventories.reservations.list, inventories.commit and inventories.release; see which scope covers which app), and an order reference of your own for step 4. The reference page for everything here is Stock.

Environment
export REVENEXX_TENANT="<TENANT_SLUG>"
export REVENEXX_API_KEY="rvxk_..."

The examples use curl and jq, and a value in angle brackets is an id from your own data.

What the platform decides and what you decide

YouThe platform
Create locations, give them a priority and an address, receive stock into themPicks the location of each reserved item, by the tenant's allocation_strategy
Send the order reference, the items and an optional ship_to hintRefuses or backorders what no location can cover, by backorder_policy
Decide when goods leave and call commitLowers on_hand and reserved at the location each hold sits at

The consequence: you cannot tell reserve "take these four from Rotterdam". You can influence the choice (priority, a ship_to hint, the order of your calls) and read where each hold landed, but not force it (as of 2026-11).

1. Model locations and priorities

A location is a place stock is kept. Create one per warehouse, store or dropship supplier. code and name are required, and code is what every stock call uses from then on.

Create two locations
curl -s -X POST https://api.revenexx.com/v1/inventories/locations \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"code":"leipzig","name":"Leipzig warehouse","type":"warehouse","priority":10,
       "address":{"country_code":"DE","address_line1":"Torstraße 1","postal_code":"04109","locality":"Leipzig"}}'

curl -s -X POST https://api.revenexx.com/v1/inventories/locations \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"code":"rotterdam","name":"Rotterdam warehouse","type":"warehouse","priority":20,
       "address":{"country_code":"NL","address_line1":"Waalhaven 2","postal_code":"3087 BK","locality":"Rotterdam"}}'

Three fields carry behaviour:

  • priority: lower is preferred. The allocator walks enabled locations in ascending order and takes the first that can cover the item. Locations that tie come back in no promised order, so give each one a distinct number if the order matters.
  • enabled: defaults to true, so a location is offered the moment you create it. A disabled location keeps its stock but is never counted in availability and never picked by a reservation. It still accepts receipts: they are booked, flagged location_disabled and answered with a warning, and the stock stays out of availability until the location is enabled.
  • address.country_code: only the nearest strategy reads it (step 2). The address is checked against the platform's address type, so a German address without a postal code is refused with 400 invalid_address and the message names the field.

type (warehouse, store, dropship, virtual) is a label. Nothing branches on it.

A new tenant already has one location, main, with priority 0. It sorts first, so on a test tenant that should draw only from the two above, take it out of service with PUT /v1/inventories/locations/<LOCATION_ID> and {"enabled": false}. Find its id with GET /v1/inventories/locations?code=main.

A new location is empty. Stock arrives by a receipt that names the location:

Receive stock
curl -s -X POST https://api.revenexx.com/v1/inventories/receive \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"location_code":"leipzig","items":[{"sku":"ACME-4711-BLK","quantity":6}]}'

curl -s -X POST https://api.revenexx.com/v1/inventories/receive \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"location_code":"rotterdam","items":[{"sku":"ACME-4711-BLK","quantity":4}]}'

The same SKU in two locations is two stock rows. Omitted, location_code falls back to the tenant's default_location_code setting, which is main until someone changes it.

A stock row's balance is never written directly.PUT /v1/inventories/stock/<ID> takes reorder_point, metadata and the availability fields. If the body also carries on_hand or reserved, those two keys are dropped without an error and the rest is saved. If the body carries nothing else, the answer is 422 and names the adjust route. To move a balance, receive, or adjust with a signed quantity and a reason (POST /v1/inventories/stock/<ID>/adjust). Moving stock from one location to another is two bookings: a negative adjustment at the old one and a receipt at the new one. There is no transfer call (as of 2026-11).

2. Pick a reservation strategy

The strategy is a tenant setting, not a request field. Read what the tenant runs now:

Read the settings
curl -s "https://api.revenexx.com/v1/settings/apps/inventories" \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  | jq '.settings | {allocation_strategy, backorder_policy, default_location_code}'

Add ?market=<MARKET_CODE> for a market's own value: all three are set per market, with the tenant value as the fallback. There is no public write route for app settings (as of 2026-11). The merchant changes them in the Inventories settings form: allocation_strategy and backorder_policy under the allocation group, default_location_code under locations. See Settings.

allocation_strategyWhich location takes an itemUse it when
priority (default)The first enabled location, by ascending priority, that holds the whole quantityOne main warehouse with fallbacks
nearestRanked by the order's ship_to hint first: the named location, then locations in the same country, then the rest. Inside a rank, by priorityStock in several countries
single_locationOne location for the whole call: the first by priority that holds every item in full. If none does, the call is refused with 422 and nothing is reserved. The exception: with backorder_policy allow or allow_per_sku and no location covering the order, each item is backordered at the location closest to covering it, which can split the order (as of 2026-11)A second parcel is a business decision, never a silent default. Pair it with deny

ship_to is part of the reserve call and is read only under nearest. Under the other two it is accepted and ignored.

ship_to
{ "ship_to": { "country": "NL", "location_code": "rotterdam" } }

ship_to.location_code is a preference, for a store the buyer picked: if that location cannot cover the item, the allocator moves on. ship_to.country is matched against each location's address.country_code, so a location stored without a country is never matched by country.

backorder_policy is the second decision: what happens when no location can cover an item.

backorder_policyWhen no location covers an item
deny (default)The whole reserve call is refused with 422, nothing reserved
allowReserved anyway, at the location with the most available stock, and available goes negative by the shortfall
allow_per_skuLike allow for items whose stock row carries metadata.backorder: true; 422 for the rest

Do not confuse the two metadata keys. metadata.backorder (no d) is a flag you set on a stock row with PUT /v1/inventories/stock/<ID>, which needs the inventories.stock.update scope. It lets that item be oversold under allow_per_sku, so give that scope only to keys that should decide it. metadata.backordered is written by the platform on a hold, and holds the quantity the shelf did not cover.

Orders converted from a cart send no ship_to. When the tenant's convert_reserves_stock setting makes the cart conversion reserve stock, the conversion sends only the order reference and the items. Under nearest such a reservation falls back to priority order (as of 2026-11). If the right warehouse depends on the delivery address, reserve from your own integration and send ship_to.

3. Read per-location availability

One call answers a whole cart, and each item carries a breakdown per enabled location:

Availability
curl -s -X POST https://api.revenexx.com/v1/inventories/availability \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"sku":"ACME-4711-BLK","quantity":8}]}' \
  | jq '.availability[0] | {available, orderable, locations: [.locations[] | {location, on_hand, reserved, available}]}'

With the stock from step 1, the item is available: 10 and orderable: true, and locations lists leipzig with available: 6 and rotterdam with available: 4. Add a location_code to the request to ask about one location only, for example what a click-and-collect store can promise. A code no location carries answers 404 unknown_location.

orderable adds the locations up, and a reservation does not. Eight units are orderable here, but under priority a reserve of eight finds no single location that holds all of them, so with backorder_policydeny it is refused with 422 and the message names the item. Read the locations array and decide the split yourself (step 4).

4. Reserve, and read where each hold landed

order_ref is a string you choose. The platform mints none, and release and commit both act on it, so keep it unique per thing you will ship together.

A · FIVE UNITSsavesB · EIGHT UNITSrefusedyour integrationorder_ref ORD-1000leipzigpriority 10, available 6rotterdampriority 20, available 4shipped from leipzigyour integrationorder_ref ORD-1001leipzigpriority 10, available 6rotterdampriority 20, available 4422, nothing reservedreserve 5commitreserve 8Lands whole at the first location that holds all 5.No location holds 8 (6 + 4 = 10): refused under deny.your integration: reserve, then commitrefused
Reserve a cart
curl -s -X POST https://api.revenexx.com/v1/inventories/reserve \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"order_ref":"ORD-1000","items":[{"sku":"ACME-4711-BLK","quantity":5}],"ship_to":{"country":"DE"}}' \
  | jq '{backordered, reservations: [.reservations[] | {sku, quantity, location_id}]}'

ship_to is read only under nearest. Under the default priority strategy this call ignores it, and the hold lands by priority.

The answer is one reservation per item, each with the full requested quantity and one location_id. A cart of three items makes three reservations, which the strategy may place at up to three locations. The call is all-or-nothing: if one item cannot be covered, nothing is reserved. An item named twice in one call is refused with 400 duplicate_item.

Reserve is not idempotent. A second call under the same order_ref adds holds, so a blind retry after a timeout reserves the stock twice. Before you retry, list the holds with GET /v1/inventories/reservations?order_ref=<ORDER_REF>. If they are there, carry on. If you cannot tell, release the reference with POST /v1/inventories/release (a second release is harmless) and reserve again.

The reservation carries a location_id, not a code. Map it with the location list:

Which location is that
curl -s "https://api.revenexx.com/v1/inventories/locations" \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  | jq '[.items[] | {id, code}]'

Read the holds of an order at any time with GET /v1/inventories/reservations?order_ref=ORD-1000. Each hold also carries an expires_at, from the tenant's reservation_ttl_minutes or the one you send, and the sweep releases a hold that runs past it.

Split one item across locations

When no single location holds the quantity and you still want to sell it, you make the split, with one reserve call per part under the same order_ref. A second call under a reference adds holds beside the first and never replaces them.

Six from the first location, two from the next
curl -s -X POST https://api.revenexx.com/v1/inventories/reserve \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"order_ref":"ORD-1001","items":[{"sku":"ACME-4711-BLK","quantity":6}]}'

curl -s -X POST https://api.revenexx.com/v1/inventories/reserve \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"order_ref":"ORD-1001","items":[{"sku":"ACME-4711-BLK","quantity":2}]}'

Take the quantities from the locations array of step 3: reserve the part the first location can cover first, so that the second call finds it used up and moves on. The result is two reservations of the same SKU at two locations under one reference. Check the location_id of each, because the allocator, not your plan, decides where each lands. Under single_location this is no way round the rule: each call is judged on its own, and a split like this is exactly what the strategy exists to stop, so use it only where the merchant accepts a second parcel.

5. Commit and ship from a location

Commit when the goods leave the building, and not before:

Commit
curl -s -X POST https://api.revenexx.com/v1/inventories/commit \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"order_ref":"ORD-1001"}'

The call takes an order_ref and nothing else: no location, no line, no quantity. It commits every active hold under the reference, each one against the stock row at its own location. That lowers on_hand and reserved there by the held quantity and writes one shipment movement per hold, at that location. For the split above the answer is {"committed": 2, "short": 0}. short counts holds that shipped against stock that was not physically there, a backorder going out early. Anything above zero means the ledger and the shelf disagree.

Two things follow:

  • Shipping in Orders names no location either. POST /v1/orders/<ORDER_ID>/ship books the quantities, a carrier and tracking, and moves no stock. Orders never writes stock, and nothing in Inventories listens to a shipment (as of 2026-11). Your integration makes both calls, and decides which parts of the order a parcel carries. See Ship, cancel and return an order.
  • A parcel per location means a reference per parcel. There is no partial commit (as of 2026-11), so goods that ship on different days or from different warehouses need holds under different references. Reserve each part under its own, for example ORD-1001/1 and ORD-1001/2, read where each landed, and commit the reference whose parcel just left.

If a reference has nothing active, commit answers 422: the hold was already shipped, released or swept. Cancelling is the other end: POST /v1/inventories/release with the order_ref gives back every active hold under it, and a second release is harmless (released: 0).

If something goes wrong

  • 422 insufficient stock although availability says orderable: true. No single location holds the whole quantity. Split it as in step 4, or ask the merchant about backorder_policy.
  • The wrong warehouse took the order under nearest. The reserve call carried no ship_to (a cart conversion never does), or the location has no address.country_code, so it was ranked with the rest and sorted by priority.
  • A ship_to.location_code was ignored. It is a preference. If that location could not cover the item, the allocator took the next one.
  • location_code on reserve did not steer anything. On reserve it only decides where a backordered item with no stock row anywhere is booked. Which location serves an item that is in stock comes from the strategy (as of 2026-11).
  • single_location split an order anyway. Backorders were allowed, see the strategy table in step 2. Use deny where one parcel is a hard rule.
  • available is negative. A backorder was reserved beyond the shelf. The shortfall sits in metadata.backordered on the hold and in metadata.shortfall on the shipment movement.
  • Commit answers 422 after a delay. The hold expired and the sweep released it. Reserve again, or send a later expires_at.

Where to go next

Was this page helpful?