API docs
Purchase orders
Permission: purchase_orders (read / write). Everything is filtered to the locations the key can see.
With write access, an outside system (a channel manager like Flxpoint, a wholesaler portal) runs the whole buying cycle: create a draft, edit it, submit it to the vendor, cancel it, and receive stock against it. The rules match the back office.
| Endpoint | Permission |
|---|---|
GET /v1/purchase-orders |
read |
GET /v1/purchase-orders/{id} |
read |
POST /v1/purchase-orders |
write |
PATCH /v1/purchase-orders/{id} |
write |
POST /v1/purchase-orders/{id}/submit |
write |
POST /v1/purchase-orders/{id}/cancel |
write |
POST /v1/purchase-orders/{id}/receive |
write (adds stock) |
The purchase order object
{
"object": "purchase_order",
"id": 77,
"number": "PO18-00019",
"status": "partial",
"location_id": 18,
"vendor_id": 34,
"vendor_reference": "ASC-SO-22817",
"flxpoint_order_id": "FXP-448120",
"ordered_on": "2026-09-20",
"expected_on": "2026-09-26",
"subtotal_cents": 26000,
"shipping_cents": 1500,
"total_cents": 27500,
"lines": [
{ "object": "purchase_order_line", "id": 511, "product_id": 617, "vendor_sku": "ASC-SSD-08", "description": "Sea Salt & Driftwood Soy Candle 8oz", "qty_ordered": 24, "qty_received": 12, "unit_cost_cents": 650 },
{ "object": "purchase_order_line", "id": 512, "product_id": 640, "vendor_sku": "ASC-SOAP-LAV", "description": "Lavender Goat Milk Soap", "qty_ordered": 60, "qty_received": 0, "unit_cost_cents": 173 }
],
"created_at": "2026-09-20T14:00:00Z",
"updated_at": "2026-09-24T17:30:00Z"
}
status is one of:
| Status | Meaning |
|---|---|
draft |
Being built. Vendor, lines and header fields can all change. |
submitted |
Sent to the vendor. Vendor and lines are frozen; header fields (dates, shipping, references, notes) can still change. |
partial |
Some stock has been received. |
received |
Everything ordered has been received. |
closed |
Closed in Black Beach POS (e.g. the vendor shorted the rest). |
cancelled |
Cancelled before anything was received. |
List and get
GET /v1/purchase-orders. Filters: status, vendor_id, flxpoint_order_id, vendor_reference, number, location_id, updated_since, limit, cursor.
GET /v1/purchase-orders/{id} returns one purchase order with its lines.
curl https://app.blackbeachpos.com/api/v1/purchase-orders?flxpoint_order_id=FXP-448120 \
-H "Authorization: Bearer ir_live_…"
Create
POST /v1/purchase-orders. Send an Idempotency-Key so a retried request never creates two purchase orders.
| Field | |
|---|---|
vendor_id |
Required. From GET /v1/vendors. |
location_id |
Where the stock will be received. Defaults to the key's only location, else the shop's default location. |
lines |
Required. 1–500 lines: { product_id or sku, qty, unit_cost_cents?, vendor_sku? }. unit_cost_cents defaults to the product's cost; vendor_sku to the product's vendor SKU. |
expected_on |
YYYY-MM-DD |
shipping_cents |
Added to the total |
vendor_reference |
The vendor's order or confirmation number |
flxpoint_order_id |
Your id for this purchase order. Must be unique in the shop: a second create with the same one is 409 DUPLICATE_PURCHASE_ORDER. |
notes |
Saved after a "Via API (key name)" line |
submit |
true to create and submit in one call |
curl -X POST https://app.blackbeachpos.com/api/v1/purchase-orders \
-H "Authorization: Bearer ir_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: po-FXP-448120" \
-d '{
"vendor_id": 34,
"location_id": 18,
"flxpoint_order_id": "FXP-448120",
"vendor_reference": "ASC-SO-22817",
"expected_on": "2026-10-01",
"lines": [
{ "sku": "CDL-SSD-08", "qty": 24, "unit_cost_cents": 650 },
{ "sku": "SOAP-LAV", "qty": 60 }
],
"submit": true
}'
Returns 201 with the purchase order (status submitted here, draft without submit).
Update
PATCH /v1/purchase-orders/{id}. Send only what changes.
- Header fields (
expected_on,ordered_on,shipping_cents,vendor_reference,flxpoint_order_id,notes) can change while the purchase order isdraftorsubmitted. vendor_idandlinescan change only while it'sdraft. Sending either on a submitted purchase order is409 PO_SUBMITTED: the vendor already has it. Cancel it and create a new one to change what was ordered.linesreplaces every line.- Anything received, closed or cancelled is
409 INVALID_STATE.
curl -X PATCH https://app.blackbeachpos.com/api/v1/purchase-orders/77 \
-H "Authorization: Bearer ir_live_…" \
-H "Content-Type: application/json" \
-d '{ "expected_on": "2026-10-03", "vendor_reference": "ASC-SO-22817-R1" }'
Submit
POST /v1/purchase-orders/{id}/submit moves a draft to submitted and stamps ordered_on (today, unless already set).
- Anything but a draft is
409 INVALID_STATE. - A draft with no lines is
422 EMPTY_PO.
Cancel
POST /v1/purchase-orders/{id}/cancel cancels a draft or submitted purchase order.
- Once anything has been received (
partial), it can't be cancelled (409 INVALID_STATE). Close it in Black Beach POS instead. received,closedandcancelledare409 INVALID_STATE.
Receive
POST /v1/purchase-orders/{id}/receive records stock arriving against a submitted or partial purchase order and posts it at once:
- Stock goes up at the purchase order's location, as
receivemovements. - Each line's
qty_receivedgoes up, and the status becomespartialorreceived. - The
receiving.postedandinventory.changedwebhooks fire.
Send an Idempotency-Key: a retried receive without one would add the stock twice.
Specific lines:
curl -X POST https://app.blackbeachpos.com/api/v1/purchase-orders/77/receive \
-H "Authorization: Bearer ir_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rcv-FXP-448120-1" \
-d '{
"reference": "UPS 1Z999AA10123456784",
"received_on": "2026-09-30",
"lines": [
{ "purchase_order_line_id": 511, "qty": 12 },
{ "purchase_order_line_id": 512, "qty": 60, "lot_number": "LAV-2610-C", "expires_on": "2027-10-01" }
]
}'
Each line is { purchase_order_line_id, qty, unit_cost_cents?, lot_number?, expires_on? }. qty must be between 1 and what's outstanding on that line (ordered minus received), else 400 VALIDATION_ERROR. unit_cost_cents overrides the purchase order's cost for this receipt.
Everything outstanding:
curl -X POST https://app.blackbeachpos.com/api/v1/purchase-orders/77/receive \
-H "Authorization: Bearer ir_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rcv-FXP-448120-2" \
-d '{ "all_outstanding": true }'
Returns 201:
{
"status": "ok",
"data": {
"object": "receiving",
"id": 41,
"number": "R18-00041",
"status": "posted",
"lines": [
{ "purchase_order_line_id": 511, "product_id": 617, "qty": 12, "unit_cost_cents": 650, "lot_number": null, "expires_on": null }
],
"skipped": [
{ "purchase_order_line_id": 512, "sku": "SOAP-LAV", "qty": 60, "reason": "lot_number_required" }
],
"purchase_order": { "object": "purchase_order", "id": 77, "status": "partial", "…": "…" }
}
}
Lot-tracked lines
A lot-tracked product's line needs the lot being received: send lot_number (up to 60 characters) and expires_on (YYYY-MM-DD) when the lot expires. Posting creates that lot at the location, or tops it up if a lot with that number is already there, exactly as the Receiving screen does. inventory.changed carries the lot.
- Naming a lot-tracked line without
lot_numberis400 VALIDATION_ERROR(lines.N.lot_number), and nothing is received. all_outstandingcan't know lot numbers, so lot-tracked lines are left out and listed inskippedwith reasonlot_number_required. Receive them by naming the line.
Serialized items are received in Black Beach POS
Receiving a serialized unit records its serial number, which staff enter per unit in Black Beach POS (Receiving). Through the API:
- Naming a serialized line in
linesis409 SERIALIZED(details.purchase_order_line_id), and nothing is received. - With
all_outstanding, serialized lines are left out and listed inskipped(reasonserialized); everything else is received. - If only serialized or lot-tracked lines are outstanding,
all_outstandinganswers409 NOTHING_OUTSTANDINGwithdetails.skipped.
Receive errors
| Status | Code | When |
|---|---|---|
| 409 | INVALID_STATE |
The purchase order is a draft (submit it first), or received, closed or cancelled |
| 409 | SERIALIZED |
A named line is a serialized product |
| 409 | NOTHING_OUTSTANDING |
Nothing is left that all_outstanding can receive |
| 400 | VALIDATION_ERROR |
Unknown purchase_order_line_id, qty outside 1…outstanding, a lot-tracked line without lot_number, or a bad expires_on |
