API docs
Inventory
Permission: inventory (read / write).
Black Beach POS is the source of truth for on-hand stock. Every quantity change, whether a sale, a receiving, a count or an API adjustment, is recorded as a stock movement. The level is the running total of those movements. A key limited to some locations only sees those locations.
A product is stocked one of three ways:
- By quantity: most items. The API can adjust and count these.
- By lot (
tracks_lots: true): each lot has a lot number, an optional expiry date and its own quantity. Candles, soaps, food and cosmetics are typical. Lots are read-only through the API, except that receiving a purchase order can create them. - By serial number (
is_serialized: true): each unit has its own serial number. Serial numbers are read-only through the API.
Levels
GET /v1/inventory: one row per product per location.
| Filter | |
|---|---|
product_id, sku |
One product |
location_id |
One location (must be one the key can see; otherwise the list is empty) |
include=lots |
Embed each lot-tracked level's lots (in stock only, earliest expiry first) |
updated_since, limit, cursor |
See Conventions |
curl "https://app.blackbeachpos.com/api/v1/inventory?location_id=18&include=lots&updated_since=2026-09-24T00:00:00Z" \
-H "Authorization: Bearer $KEY"
{
"object": "inventory_level",
"product_id": 640,
"location_id": 18,
"qty_on_hand": 36,
"qty_reserved": 0,
"qty_available": 36,
"reorder_point": 12,
"bin_location": "Shelf B",
"updated_at": "2026-09-25T01:48:03Z",
"sku": "SOAP-LAV",
"name": "Lavender Goat Milk Soap",
"tracks_lots": true,
"lots": [
{ "object": "inventory_lot", "id": 62, "product_id": 640, "location_id": 18, "lot_number": "LAV-2510-A", "expires_on": "2026-10-01", "qty_on_hand": 10, "status": "expiring", "days_to_expiry": 7, "cost_cents": 180, "created_at": "2026-09-01T14:00:00Z", "updated_at": "2026-09-20T17:31:02Z" },
{ "object": "inventory_lot", "id": 63, "product_id": 640, "location_id": 18, "lot_number": "LAV-2609-B", "expires_on": "2027-08-13", "qty_on_hand": 26, "status": "in_stock", "days_to_expiry": 322, "cost_cents": 185, "created_at": "2026-09-10T15:12:40Z", "updated_at": "2026-09-24T19:02:11Z" }
]
}
qty_available is qty_on_hand minus qty_reserved. Use qty_available when you publish stock to a sales channel. For a lot-tracked product the level is the sum of its lots at that location.
Lots
GET /v1/inventory/lots: one row per lot per location. Read-only.
| Filter | |
|---|---|
product_id, sku |
One product |
location_id |
One location |
lot_number |
Exact match |
status |
in_stock, expiring (in stock and expiring soon), expired (in stock past its date), empty |
expires_before |
YYYY-MM-DD; lots expiring before that date |
updated_since, limit, cursor |
{
"object": "inventory_lot",
"id": 62,
"product_id": 640,
"location_id": 18,
"lot_number": "LAV-2510-A",
"expires_on": "2026-10-01",
"qty_on_hand": 10,
"status": "expiring",
"days_to_expiry": 7,
"cost_cents": 180,
"created_at": "2026-09-01T14:00:00Z",
"updated_at": "2026-09-20T17:31:02Z"
}
expires_onanddays_to_expiryarenullfor a lot with no expiry date.- Lots come in by receiving: the Receiving screen in Black Beach POS, or receiving a purchase order with a
lot_number. - They go out on sales, earliest expiry first unless the sale names a lot. An order line can send
lot_id; order lines and stock movements report the lot they used.
Adjust
POST /v1/inventory/adjust (write) adds to or removes from on-hand stock of a product stocked by quantity.
| Field | |
|---|---|
product_id or sku |
Required (one of them) |
location_id |
Defaults to the key's only location, else the shop's default location |
qty_delta |
Required, non-zero integer; negative removes |
reason |
adjust (default), damage, return_to_vendor, count, receive |
note |
Up to 200 characters. Stored as API (<key name>): <note> |
unit_cost_cents |
Optional cost for the movement |
curl -X POST https://app.blackbeachpos.com/api/v1/inventory/adjust \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: dropship-return-88213" \
-d '{ "sku": "CDL-SSD-08", "location_id": 18, "qty_delta": 5, "reason": "receive", "note": "Flxpoint PO 88213" }'
Returns the level after the change.
Set a count
POST /v1/inventory/set (write) sets an absolute quantity, as from a physical count. Send product_id or sku, location_id, qty_on_hand (≥ 0) and an optional note. It's recorded as a count movement for the difference.
Both writes are refused:
- On serialized products:
409 SERIALIZED. Units come in by receiving, where each serial number is entered, and go out on sales. - On lot-tracked products:
409 LOT_TRACKED. Lots are counted and adjusted per lot in Black Beach POS (Inventory › Lot numbers). - On products that don't track inventory:
409 NOT_TRACKED. - When the result would go below zero and the shop doesn't allow negative stock:
409 INSUFFICIENT_STOCK.
Both writes accept Idempotency-Key. Always send one on adjustments, because a retried adjustment without it counts twice.
Serialized units
GET /v1/inventory/serialized lists individual units with their serial numbers. Serial numbers are read-only.
| Filter | |
|---|---|
product_id, sku |
One product |
status |
in_stock, reserved, sold, returned_to_vendor, returned_to_consignor, in_repair, consignment, stolen_lost |
serial_number |
Exact match |
location_id, updated_since, limit, cursor |
{
"object": "serialized_item",
"id": 311,
"product_id": 702,
"location_id": 18,
"serial_number": "WE-DIF-00417",
"status": "in_stock",
"condition": "new",
"price_cents": null,
"cost_cents": 4200,
"acquired_on": "2026-09-15",
"is_consigned": false,
"updated_at": "2026-09-24T06:22:23Z",
"sku": "DIF-ELEC-01"
}
price_cents is the unit's own price when it differs from the product's (used items, consignments). Otherwise it's null.
Movements
GET /v1/inventory/movements is the stock ledger, oldest first. Filter by product_id, sku, location_id or updated_since; for this list, updated_since filters on creation time.
{ "object": "stock_movement", "id": 90112, "product_id": 640, "location_id": 18, "lot_id": 62, "qty_delta": -2, "qty_after": 34, "reason": "sale", "reference_type": "sale", "reference_id": 5521, "created_at": "2026-09-25T02:10:44Z" }
| Field | Values |
|---|---|
lot_id |
The lot that moved, for lot-tracked products; otherwise null |
reason |
sale, sale_void, refund, receive, adjust, count, damage, return_to_vendor, online_sale, online_refund, migration, consignment, trade_in, consignment_in, consignment_return, transfer_in, transfer_out |
reference_type |
sale, return, receiving, lot, api, …, or null |
reference_id |
Set for sale, return and receiving. A sale reference is an order id. |
For real-time stock changes, subscribe to the inventory.changed webhook. For lot-tracked products the event also carries the lot that moved.
