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 distributor 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": "PO93-00019",
"status": "partial",
"location_id": 93,
"vendor_id": 155,
"vendor_reference": "LIP-SO-22817",
"flxpoint_order_id": "FXP-448120",
"ordered_on": "2026-09-20",
"expected_on": "2026-09-26",
"subtotal_cents": 263400,
"shipping_cents": 0,
"total_cents": 263400,
"lines": [
{ "object": "purchase_order_line", "id": 512, "product_id": 2053, "vendor_sku": "GLPA195S203", "description": "Glock 19 Gen 5 9mm", "qty_ordered": 6, "qty_received": 4, "unit_cost_cents": 43900 }
],
"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 Iron Register (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.ironregister.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.ironregister.com/api/v1/purchase-orders \
-H "Authorization: Bearer ir_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: po-FXP-448120" \
-d '{
"vendor_id": 155,
"location_id": 93,
"flxpoint_order_id": "FXP-448120",
"vendor_reference": "LIP-SO-22817",
"expected_on": "2026-10-01",
"lines": [
{ "sku": "AMM-9-115-50", "qty": 40, "unit_cost_cents": 1100 },
{ "sku": "GLK-G19G5", "qty": 2 }
],
"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.ironregister.com/api/v1/purchase-orders/77 \
-H "Authorization: Bearer ir_live_…" \
-H "Content-Type: application/json" \
-d '{ "expected_on": "2026-10-03", "vendor_reference": "LIP-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 Iron Register 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.ironregister.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": 24 } ]
}'
Each line is { purchase_order_line_id, qty, unit_cost_cents? }. 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.ironregister.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": "R93-00041",
"status": "posted",
"lines": [
{ "purchase_order_line_id": 511, "product_id": 2062, "qty": 16, "unit_cost_cents": 1100 }
],
"skipped": [
{ "purchase_order_line_id": 512, "sku": "GLK-G19G5", "qty": 2, "reason": "serialized" }
],
"purchase_order": { "object": "purchase_order", "id": 77, "status": "partial", "…": "…" }
}
}
Serialized items and firearms are received in Iron Register
Receiving a serialized unit records its serial number, and for a firearm that is the acquisition entry in the bound book. That is done in Iron Register (Receiving), where staff enter each serial. 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; everything else is received. - If only serialized lines are outstanding, the response is
409 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 (non-serialized) is left to receive |
| 400 | VALIDATION_ERROR |
Unknown purchase_order_line_id, or qty outside 1…outstanding |
Received serial numbers are never included in any API response or webhook; they're acquisition records.