Purchasing & Receiving API
Required scope: purchasing:read
These endpoints expose the buying half of the chain: what was ordered from a supplier, what the supplier invoiced, and what actually arrived on the dock. Without them an external dashboard can see the catalogue, the stock and the sales, but not why the stock number moved — so it can never close the loop on a short shipment or a mis-billed case.
There is no write side. Raising and receiving purchase orders stays in the Brother POS admin, where the approval and stock-posting steps live. Any non-GET request returns 403 Forbidden.
List Purchase Orders
GET /api/v1/purchase_orders
Returns a paginated list of purchase orders, newest first (by creation time). Every order includes its line items.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number (default: 1) |
per_page | integer | Results per page (default: 50, max: 100) |
status | string | Filter by status — see the status table below. An unrecognised value is ignored. |
supplier_id | integer | Only orders from this supplier |
updated_since | ISO 8601 | Only orders changed after this timestamp — the cursor for a reconciliation pull |
with_variance | boolean | Only orders with at least one line that came up short, over, or unexpected |
An updated_since value that cannot be parsed as a timestamp returns 422 Unprocessable Entity rather than silently ignoring the filter.
Order Statuses
| Status | Meaning |
|---|---|
pending | Order placed, waiting on the supplier |
confirmed | Supplier confirmed the order |
shipped | Supplier shipped it |
partially_received | Some lines received, others still outstanding |
received | The whole order has been received |
cancelled | Order was cancelled |
Example
Find every order still carrying a receiving discrepancy:
curl -H "X-API-Key: bpos_..." \
"https://yourstore.brotherpos.ca/api/v1/purchase_orders?with_variance=true&per_page=50"
Response
{
"data": [
{
"id": 412,
"order_number": "PO-20260810-0003",
"status": "received",
"supplier": { "id": 7, "name": "Prairie Wholesale" },
"subtotal": 1840.0,
"tax_amount": 239.2,
"shipping_cost": 45.0,
"total": 2124.2,
"ordered_at": "2026-08-10T15:04:00.000Z",
"received_at": "2026-08-14T18:22:00.000Z",
"created_at": "2026-08-10T14:58:00.000Z",
"updated_at": "2026-08-14T18:22:00.000Z",
"has_variance": true,
"line_items": [ ... ]
}
],
"pagination": { "current_page": 1, "per_page": 50, "total_count": 8, "total_pages": 1 },
"meta": { "request_id": "...", "timestamp": "2026-08-19T12:00:00Z" }
}
| Field | Type | Notes |
|---|---|---|
order_number | string | Human-readable reference, unique per store |
supplier | object or null | null on orders with no supplier attached |
subtotal, tax_amount, shipping_cost, total | number | Order-level money, as JSON numbers |
ordered_at, received_at | timestamp or null | received_at is set when the order is marked received |
has_variance | boolean | True if any line is short, over or unexpected |
line_items | array | Always present — see below |
Get a Purchase Order
GET /api/v1/purchase_orders/:id
Returns a single order in exactly the same shape, including its line items. The purchase_order.received webhook carries this same shape in its data.
Line Items: The Three-Way Match
Each line carries three separate quantities, and the differences between them are three different arguments to have with three different people.
| Quantity | Field | Meaning |
|---|---|---|
| Ordered | quantity_ordered | What you asked the supplier for |
| Invoiced | quantity_invoiced | What the supplier billed you for — null until a supplier document is applied to the order |
| Received | quantity_received | What was actually counted in |
The variances are reported separately rather than collapsed into one number:
| Field | Calculation | What it tells you |
|---|---|---|
invoice_variance | invoiced − ordered | The supplier billed a different quantity than you ordered. A purchasing problem. null with no invoice. |
receiving_variance | received − invoiced | You were billed for units that did not turn up (or received units nobody billed). A billing dispute. null with no invoice. |
fulfillment_variance | received − ordered | You did not get what you asked for. A shipping problem. Always present. |
variance_value | −(receiving variance) × unit_cost_invoiced, falling back to unit_price | The money at stake. Positive means the supplier owes you. null with no invoice. |
invoice_variance is 0.0 — the bill matches the order. receiving_variance is -4.0 and fulfillment_variance is -4.0 — four units are missing, and you were charged for them. At a $12 invoiced cost, variance_value comes back as 48.0: the supplier owes you $48.
Variance Status
variance_status summarises received-against-ordered:
| Value | Meaning |
|---|---|
pending | Nothing received on this line yet |
ok | Received exactly what was ordered |
short | Received less than ordered |
over | Received more than ordered |
unexpected | Arrived without being ordered (nothing was ordered on this line) |
The with_variance=true filter selects orders holding a short, over or unexpected line. It is built on received-vs-ordered, so it will not pull in a line that is purely an invoice discrepancy — for instance one invoiced for 8 with 0 received still reads pending. To catch billing gaps as well, pull the orders and check invoice_variance per line yourself.
Match Status
match_status is a different question — not "did the right quantity arrive" but "do we know which product this line is":
| Value | Meaning |
|---|---|
matched | The line is linked to a product in your catalogue |
unmatched | Nothing in the catalogue has been linked yet — staff need to resolve it |
create_new | Flagged to create a new product when the order is received |
Line Item Fields
{
"id": 9021,
"product_id": 155,
"product_name": "Blue Dream 3.5g",
"sku": "BD-35",
"unit_type": "unit",
"quantity_ordered": 24.0,
"quantity_invoiced": 24.0,
"quantity_received": 20.0,
"unit_price": 12.0,
"unit_cost_invoiced": 12.0,
"line_total": 288.0,
"variance_status": "short",
"invoice_variance": 0.0,
"receiving_variance": -4.0,
"fulfillment_variance": -4.0,
"variance_value": 48.0,
"match_status": "matched",
"supplier_document_id": 88
}
| Field | Type | Notes |
|---|---|---|
product_id | integer or null | Null on an unmatched line |
product_name, sku | string | Taken from the order's snapshot of the product when available, so they still read correctly if the catalogue changes later; otherwise the linked product's current values. Fall back to "Unknown Product" / "N/A". |
unit_price | number | The cost you ordered at |
unit_cost_invoiced | number or null | The cost the supplier actually billed |
supplier_document_id | integer or null | The invoice or packing document this line was matched against |
Keeping a Dashboard in Sync
Subscribe to purchase_order.received so your system refreshes when a delivery lands, and back it with a scheduled pull using updated_since to catch anything the webhook missed. The reasoning is in Recommended Sync Architecture.
curl -H "X-API-Key: bpos_..." \
"https://yourstore.brotherpos.ca/api/v1/purchase_orders?updated_since=2026-08-19T02:00:00Z"
Troubleshooting
| Symptom | Cause |
|---|---|
403 Forbidden, "Insufficient scope. Required: purchasing:read" | The key is missing the purchasing:read scope. Scopes cannot be added to an existing key — create a new key under Settings → Developer API with the scope. |
403 Forbidden on a POST/PATCH | Expected — these endpoints are read-only via API key. |
422 with "updated_since must be a valid timestamp" | Send an ISO 8601 timestamp, e.g. 2026-08-19T02:00:00Z. |
Invoice quantities are all null | No supplier document has been applied to the order yet, so there is nothing to match against. |
status filter returns every order | The value is not one of the statuses above, so it was ignored. |