Documentation

Inventory

Stock levels, multi-location tracking, reservations, fulfillment, transfers, and low-stock alerts.

The inventory module tracks stock at the SKU level across one or more locations. Every sale, return, transfer, or count writes a transaction; current stock is the running sum. The cart layer reserves stock at add-to-cart time so a popular SKU doesn't double-sell during a flash sale.

Reading stock

GET/storefront/inventory/sku/:skuJWT
GET/storefront/inventory/sku/:sku/location/:locationIdJWT
GET/storefront/inventory/location/:locationIdJWT
GET/storefront/inventory/alerts/low-stockJWT
GET/storefront/inventory/statsJWT
GET/storefront/inventory/sku/:sku/transactionsJWT

The first endpoint returns total available across every location, broken down per location. The second returns just one location. Low-stock alerts are SKU/location pairs at or below their reorderLevel. Stats roll up the catalogue: total units on hand, total value, units sold last N days.

// GET /storefront/inventory/sku/SNK-AIR-1-9-BLK
{
  "sku": "SNK-AIR-1-9-BLK",
  "available": 42,
  "reserved": 5,
  "onHand": 47,
  "byLocation": [
    { "locationId": "loc-warehouse-1", "available": 30, "reserved": 3 },
    { "locationId": "loc-store-sf", "available": 12, "reserved": 2 }
  ],
  "reorderLevel": 10,
  "reorderQuantity": 50
}

onHand = available + reserved. available is what a new add-to-cart can grab.

Movements

POST/storefront/inventoryJWT
POST/storefront/inventory/adjustJWT
POST/storefront/inventory/reserveJWT
POST/storefront/inventory/releaseJWT
POST/storefront/inventory/fulfillJWT
POST/storefront/inventory/returnJWT
POST/storefront/inventory/countJWT

The base POST creates a stock record (initial seed). The others are typed transactions:

EndpointEffectUsed by
adjustManual +/- with a reason codeCycle counts, write-offs, found stock
reserveMove available → reservedCart add, order pending
releaseMove reserved → availableCart abandonment, order cancel
fulfillDecrement reserved (commits the sale)Shipment created
returnIncrement availableRMA inspection passes
countSet absolute on-hand to a counted valuePhysical inventory
// reserve 2 units for an order
await fetch('/api/storefront/inventory/reserve', {
  method: 'POST',
  headers: { orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
  body: JSON.stringify({
    sku: 'SNK-AIR-1-9-BLK',
    locationId: 'loc-warehouse-1',
    quantity: 2,
    referenceType: 'order',
    referenceId: 'ord-12345',
  }),
});

Reservations carry a reference so they can be released by reference rather than by ID — the order-cancellation flow does this without needing to remember the reservation ID.

Reservation and stock-rule semantics

The reservation model prevents oversells but is timed:

  • Cart adds reserve immediately and the reservation expires when the cart is cleared or after the org-configured TTL (default 60 minutes).
  • Order placement converts the reservation into a "pending fulfillment" that doesn't expire.
  • Shipment via the logistics module calls fulfill, decrementing reserved.

If you sell at very high volume, configure the cart-reservation TTL to match your typical checkout time and not much longer. A long TTL means a flash-sale cart hoards stock the customer never buys.

Transfers between locations

POST/storefront/inventory/transfersJWT
GET/storefront/inventory/transfersJWT
GET/storefront/inventory/transfers/:transferIdJWT
PUT/storefront/inventory/transfers/:transferId/shipJWT
PUT/storefront/inventory/transfers/:transferId/receiveJWT
PUT/storefront/inventory/transfers/:transferId/cancelJWT

Transfers are a paired transaction: a ship debits the source location, a receive credits the destination, and the transit-stock value sits on the transfer record between the two events. Cancellation before ship reverses without affecting either location.

Low-stock automation

The low-stock alert endpoint feeds an Automation flow trigger called inventory.low-stock. Wire it to:

  • email the buyer
  • create a CRM task on the supplier contact
  • post to Slack via the broadcast channel
  • raise a purchase order in your ERP

Multi-location strategy

If your storefront is single-location, set one location ID and ignore the per-location endpoints. The aggregate endpoints work identically. If you have many locations, the order routing logic (in the storefront service) picks the closest fulfilling location to the customer's shipping address by default; you can override with a manual fulfillFromLocationId on the order.

Inventory transactions are the source of truth. The available cache is rebuilt on every read from the transaction sum — there's no drift between the displayed stock and the underlying ledger.