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_on and days_to_expiry are null for 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.