Inventory balances
Review current physical stock by product and warehouse, focus on an availability state, and use complete product and product-and-warehouse stock-out counts.
Use GET /inventory-balances when a workflow needs the current physical balance for one product, a product family, one warehouse, or the complete visible inventory population.
Access
The endpoint requires inventory:read plus the acting user's current access to view Products and Lots. GET /me reports the result through effectiveCapabilities.inventory.balances.view and effectiveCapabilities.inventory.balances.currentStateRefresh.
curl "$SHELFCYCLE_API_BASE_URL/inventory-balances?productFamilyId=product-family-id&warehouseId=warehouse-id&limit=25" \
-H "Authorization: Bearer $SHELFCYCLE_API_KEY" \
-H "X-ShelfCycle-Client: Inventory availability dashboard"
Filters
| Parameter | Use it for |
|---|---|
productId | One exact product. |
productFamilyId | Every matching product in one family. |
warehouseId | One exact physical warehouse. |
availabilityState | zero_on_hand, fully_allocated, or positive_available. |
limit | Rows per page from 1 to 100. The default is 25. |
Filters can be combined on the first request. An exact active and visible productId plus warehouseId request can return a zero row when no balance activity is present for that pair, making the zero explicit rather than leaving the match uncertain.
Availability states are based on the complete product-and-warehouse balance:
zero_on_handmeans on hand is zero.fully_allocatedmeans on hand is positive and available is zero.positive_availablemeans available is greater than zero.
To find every product-and-warehouse position with nothing available, request zero_on_hand and fully_allocated separately or use the complete meta.census.pairs.zeroAvailable count.
Read a balance row
{
"data": [
{
"type": "inventory_balance",
"product": {
"id": "product-id",
"code": "IPA-DRUM",
"archived": false,
"family": { "id": "product-family-id", "name": "Isopropyl Alcohol" }
},
"warehouse": {
"id": "warehouse-id",
"name": "Main Warehouse",
"archived": false
},
"basis": {
"type": "package_count",
"unitOfMeasure": "GALLON",
"packaging": { "id": "packaging-id", "name": "55 gallon drum" }
},
"onHand": "24",
"allocated": "6",
"available": "18",
"quarantinedOnHand": "2"
},
{
"type": "inventory_balance",
"product": {
"id": "variable-product-id",
"code": "IPA-BULK",
"archived": false,
"family": { "id": "product-family-id", "name": "Isopropyl Alcohol" }
},
"warehouse": {
"id": "warehouse-id",
"name": "Main Warehouse",
"archived": false
},
"basis": {
"type": "base_uom",
"unitOfMeasure": "POUND",
"packaging": { "id": "base-packaging-id", "name": "Tank truck" }
},
"onHand": "12500.5",
"allocated": "2500",
"available": "10000.5",
"quarantinedOnHand": "0"
}
],
"meta": {
"populationCount": 2,
"returnedRows": 2,
"hasMore": false,
"nextCursor": null,
"ordering": "product_id_asc_warehouse_id_asc",
"observedAt": "2026-08-24T16:30:00.000Z",
"currentState": true,
"snapshotIsolatedAcrossRequests": false,
"census": {
"population": "participating_physical_inventory_pairs",
"scope": "entity_filters_excluding_availability",
"pairs": {
"zeroOnHand": 12,
"fullyAllocated": 3,
"positiveAvailable": 27,
"zeroAvailable": 15,
"archived": 1
},
"products": {
"withParticipatingPairs": 36,
"noneAvailableInScope": 11
}
},
"requestId": "request-id"
}
}
All quantities are decimal strings. available is onHand less allocated. quarantinedOnHand identifies the on-hand quantity currently in quarantine; it is part of on-hand, not an additional quantity.
The basis object tells you how to interpret every quantity on that row:
package_countmeans the values count the named package and unit of measure.base_uommeans the values use the product's named base unit, with the packaging record included as context.
Keep each row's basis and unit with its quantities. Compare or add values only when the product and quantity basis make that operation meaningful.
Continue larger results
When meta.hasMore is true, call the endpoint again with meta.nextCursor by itself:
curl "$SHELFCYCLE_API_BASE_URL/inventory-balances?cursor=$NEXT_CURSOR" \
-H "Authorization: Bearer $SHELFCYCLE_API_KEY"
Do not combine a cursor with the original filters. The cursor retains the filter set, page size, ordering, organization, and API key needed for continuation. If a cursor is no longer valid, restart from the original filters.
populationCount reports the complete matched population, while returnedRows, hasMore, and nextCursor describe the current page and continuation.
Count stock outs
Use meta.census for complete counts instead of counting the current page:
products.noneAvailableInScopecounts products with no available stock anywhere in the selected product, family, and warehouse scope.pairs.zeroAvailablecounts product-and-warehouse positions with no available stock. It equalszeroOnHandplusfullyAllocated.products.withParticipatingPairscounts products with physical inventory records in the selected scope.
Keep the product and position counts distinct in every summary. For the example above, say "11 products have no available stock across 15 product-and-warehouse positions."
The census follows the product, product-family, and warehouse filters but is calculated before availabilityState. That lets a filtered page of physically empty or fully allocated positions retain the complete stock-out summary for the selected scope.
Products without a physical inventory record are not part of the balance census. Under a warehouse filter, noneAvailableInScope means the product has no available stock at the selected warehouse.
Use products.noneAvailableInScope for the product count and pairs.zeroAvailable for the product-and-warehouse-position count. Use the availability filter only when the workflow also needs the matching rows.
Refresh current state
Each response includes observedAt, currentState: true, and snapshotIsolatedAcrossRequests: false. Treat it as the current view observed at that time. For a complete refresh, restart from the original filters and continue through the newly returned cursors.
Because inventory can change between pages, deduplicate by the product and warehouse ids when reconciling a refresh. Broader reads return current participating pairs. Archived products or warehouses with a nonzero balance remain clearly marked, while archived zero pairs are omitted. Quarantined quantities remain part of on hand when balances and availability states are calculated.
Use an exact product and warehouse query for an availability check. Use product-family or warehouse filters for operational views, and periodically restart the original request to refresh the complete current population.