Purchase orders
Esta página aún no está disponible en tu idioma.
The purchase_orders resource holds inventory purchase records. Creating a record immediately adds its line items to stock, as the app’s manual receiving flow does. Use it to receive parts and supplies, retrieve purchase history and confirm quantities on recorded orders. Each record can link to a vendor and a receiving location.
Scope required: purchase_orders.read for list/get, purchase_orders.write for create/receive.
The purchase order object
Section titled “The purchase order object”{ "object": "purchase_order", "id": 412, "vendor_id": 7, "vendor_name": "iFixit Parts", "invoice_number": "INV-2025-8841", "invoice_date": "2025-11-15", "status": "received", "source": "api", "subtotal": { "amount_cents": 18500, "amount": "185.00", "currency": "USD" }, "tax": { "amount_cents": 1665, "amount": "16.65", "currency": "USD" }, "shipping": { "amount_cents": 999, "amount": "9.99", "currency": "USD" }, "total": { "amount_cents": 21164, "amount": "211.64", "currency": "USD" }, "line_item_count": 3, "location_id": 2, "tracking_carrier": null, "tracking_number": null, "tracking_last_event": null, "tracking_estimated_delivery": null, "notes": "Rush order for holiday week", "delivered_at": null, "reconciled_at": "2025-11-15T18:22:00.000Z", "created_at": "2025-11-15T17:55:00.000Z"}| Field | Type | Description |
|---|---|---|
id | integer | Purchase order ID |
vendor_id | integer|null | Linked vendor ID |
vendor_name | string|null | Vendor display name (can be set without a vendor_id) |
invoice_number | string|null | Supplier invoice number |
invoice_date | string|null | Date on the supplier invoice (YYYY-MM-DD) |
status | string | received, pending, or reconciled |
source | string | How the PO was created: manual, api, etc. |
subtotal | object | Sum of line item costs, money object |
tax | object | Tax amount, money object |
shipping | object | Shipping amount, money object |
total | object | subtotal + tax + shipping, money object |
line_item_count | integer | Number of visible line items |
location_id | integer|null | Receiving location ID |
tracking_carrier | string|null | Shipping carrier, if tracked |
tracking_number | string|null | Tracking number |
tracking_last_event | string|null | Latest tracking event description |
tracking_estimated_delivery | string|null | ISO-8601 estimated delivery timestamp |
notes | string|null | Free-text notes on the order |
delivered_at | string|null | ISO-8601 timestamp when marked delivered |
reconciled_at | string|null | ISO-8601 timestamp when reconciled into inventory |
created_at | string | ISO-8601 creation timestamp |
Money objects
Section titled “Money objects”All money fields (subtotal, tax, shipping, total, unit_cost, total_cost) are money objects:
{ "amount_cents": 18500, "amount": "185.00", "currency": "USD" }All money values in request bodies use *_cents fields (integer cents). The API converts to/from internal dollar-float storage.
The purchase order detail object
Section titled “The purchase order detail object”GET /purchase_orders/:id and POST /purchase_orders return a detail object that includes the line items:
{ "object": "purchase_order", "id": 412, "vendor_id": 7, "vendor_name": "iFixit Parts", "status": "received", "subtotal": { "amount_cents": 18500, "amount": "185.00", "currency": "USD" }, "tax": { "amount_cents": 1665, "amount": "16.65", "currency": "USD" }, "shipping": { "amount_cents": 999, "amount": "9.99", "currency": "USD" }, "total": { "amount_cents": 21164, "amount": "211.64", "currency": "USD" }, "line_item_count": 3, "location_id": 2, "notes": null, "reconciled_at": "2025-11-15T18:22:00.000Z", "created_at": "2025-11-15T17:55:00.000Z", "line_items": [ { "object": "purchase_order_line_item", "id": 901, "part_id": 55, "description": "iPhone 14 Screen Assembly", "qty": 5, "qty_received": 5, "unit_cost": { "amount_cents": 3200, "amount": "32.00", "currency": "USD" }, "total_cost": { "amount_cents": 16000, "amount": "160.00", "currency": "USD" }, "ticket_id": null, "created_at": "2025-11-15T17:55:00.000Z" } ]}Line item visibility: line items linked to hidden or soft-deleted tickets are excluded from line_items and not counted in line_item_count.
| Line item field | Type | Description |
|---|---|---|
id | integer | Line item ID |
part_id | integer|null | Linked product/part ID |
description | string|null | Line item description |
qty | integer | Quantity ordered |
qty_received | integer|null | Quantity actually received (set during reconciliation) |
unit_cost | object | Cost per unit, money object |
total_cost | object | qty × unit_cost, money object |
ticket_id | integer|null | Ticket this line item is linked to, if any |
created_at | string | ISO-8601 creation timestamp |
List purchase orders
Section titled “List purchase orders”GET /api/v1/purchase_ordersReturns a cursor-paginated list of purchase orders, ordered by ID ascending.
Scope required: purchase_orders.read
Query parameters
Section titled “Query parameters”| Parameter | Type | Description |
|---|---|---|
vendor_id | integer | Filter by vendor ID (positive integer ≤ 2,147,483,647; out-of-range values return 400 invalid_query) |
status | string | Filter by status: received, pending, reconciled |
location_id | integer | Filter by receiving location ID (positive integer ≤ 2,147,483,647; out-of-range values return 400 invalid_query) |
q | string | Search invoice number or vendor name (substring match) |
limit | integer | Page size, 1–100 (default: 20) |
cursor | string | Opaque cursor from a previous response’s next_cursor |
curl "https://app.benchkey.com/api/v1/purchase_orders?status=received&limit=20" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch( "https://app.benchkey.com/api/v1/purchase_orders?status=received&limit=20", { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } });const { data, has_more, next_cursor } = await res.json();Response
Section titled “Response”{ "object": "list", "data": [ { "object": "purchase_order", "id": 412, "vendor_id": 7, "vendor_name": "iFixit Parts", "invoice_number": "INV-2025-8841", "status": "received", "subtotal": { "amount_cents": 18500, "amount": "185.00", "currency": "USD" }, "tax": { "amount_cents": 1665, "amount": "16.65", "currency": "USD" }, "shipping": { "amount_cents": 999, "amount": "9.99", "currency": "USD" }, "total": { "amount_cents": 21164, "amount": "211.64", "currency": "USD" }, "line_item_count": 3, "location_id": 2, "created_at": "2025-11-15T17:55:00.000Z" } ], "has_more": false, "next_cursor": null}See Pagination for how to page through results.
Retrieve a purchase order
Section titled “Retrieve a purchase order”GET /api/v1/purchase_orders/:idReturns a single purchase order with its full line item list.
Scope required: purchase_orders.read
curl "https://app.benchkey.com/api/v1/purchase_orders/412" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch( "https://app.benchkey.com/api/v1/purchase_orders/412", { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } });const order = await res.json();Response
Section titled “Response”Returns the purchase order detail object. Returns 404 if not found, 400 if :id is not a valid integer.
Create a purchase order
Section titled “Create a purchase order”POST /api/v1/purchase_ordersCreates an inventory purchase record and immediately commits the line items to inventory stock, the equivalent of the manual receiving flow in the BenchKey app. Returns 201 with the full detail object on success.
Scope required: purchase_orders.write
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
items | array | Yes | Line items to receive (at least one) |
vendor_id | integer | No | Link to an existing vendor |
vendor_name | string | No | Vendor display name (used when vendor_id is absent or for display override) |
invoice_number | string | No | Supplier invoice number |
invoice_date | string | No | Invoice date (YYYY-MM-DD) |
location_id | integer | No | Receiving location |
tax_cents | integer ≥ 0 | No | Tax amount in integer cents |
shipping_cents | integer ≥ 0 | No | Shipping amount in integer cents |
notes | string | No | Free-text notes |
Line item fields (items[])
Section titled “Line item fields (items[])”| Field | Type | Required | Description |
|---|---|---|---|
part_id | integer | No* | Existing product/part ID. Either part_id or name is required |
name | string | No* | Part name. Required when part_id is not provided, creates a new part if no SKU match is found |
sku | string | No | Part SKU (used to match an existing part when part_id is absent) |
description | string | No | Line item description (defaults to part name) |
qty | integer ≥ 1 | Yes | Quantity to receive |
unit_cost_cents | integer ≥ 0 | Yes | Unit cost in integer cents |
ticket_id | integer ≥ 1 | No | Link this line item to a ticket (must be a positive integer). The ticket must exist and be visible (not hidden or soft-deleted); a nonexistent or hidden ticket returns 404 not_found |
curl -X POST "https://app.benchkey.com/api/v1/purchase_orders" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ -d '{ "vendor_id": 7, "invoice_number": "INV-2025-8841", "invoice_date": "2025-11-15", "location_id": 2, "tax_cents": 1665, "shipping_cents": 999, "items": [ { "part_id": 55, "qty": 5, "unit_cost_cents": 3200 }, { "name": "USB-C Charging Port", "sku": "UC-PORT-001", "qty": 10, "unit_cost_cents": 850 } ] }'Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/purchase_orders", { method: "POST", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Content-Type": "application/json", }, body: JSON.stringify({ vendor_id: 7, invoice_number: "INV-2025-8841", invoice_date: "2025-11-15", location_id: 2, tax_cents: 1665, shipping_cents: 999, items: [ { part_id: 55, qty: 5, unit_cost_cents: 3200 }, { name: "USB-C Charging Port", sku: "UC-PORT-001", qty: 10, unit_cost_cents: 850 }, ], }),});const order = await res.json(); // 201, full detail object with line_itemsResponse
Section titled “Response”Returns 201 with the purchase order detail object.
Receive / reconcile a purchase order
Section titled “Receive / reconcile a purchase order”POST /api/v1/purchase_orders/:id/receiveMarks a purchase order received and optionally reconciles per-line quantities against what was actually delivered. If you omit the items array (or send an empty body), all line items are accepted at their originally ordered quantities.
Use this endpoint to receive an existing pending order, such as one saved with Record order in the app, and confirm the quantities that actually arrived.
Scope required: purchase_orders.write
Path parameters
Section titled “Path parameters”| Parameter | Description |
|---|---|
id | Purchase order ID |
Request body (optional)
Section titled “Request body (optional)”{ "items": [ { "id": 901, "qty_received": 4 }, { "id": 902, "qty_received": 10 } ]}| Field | Type | Required | Description |
|---|---|---|---|
items | array | No | Per-line overrides. Omit entirely (or omit the field) to accept all lines at ordered quantities. Supplying items: [] (an empty array) returns 400 invalid_field, if you intend to receive all, omit the field |
items[].id | integer | Yes (per item) | Line item ID |
items[].qty_received | integer ≥ 0 | No | Actual quantity received; omit to use ordered qty |
items[].reconcile_note | string | No | Optional note for this line (max 5,000 characters) |
curl -X POST "https://app.benchkey.com/api/v1/purchase_orders/412/receive" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "id": 901, "qty_received": 4 } ] }'Node.js
Section titled “Node.js”const res = await fetch( "https://app.benchkey.com/api/v1/purchase_orders/412/receive", { method: "POST", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Content-Type": "application/json", }, body: JSON.stringify({ items: [{ id: 901, qty_received: 4 }], }), });const order = await res.json(); // updated detail objectResponse
Section titled “Response”Returns 200 with the updated purchase order detail object. Returns 404 if the PO does not exist.
Error codes
Section titled “Error codes”| HTTP status | Code | Meaning |
|---|---|---|
400 | invalid_id | :id path parameter is not a valid positive integer |
400 | missing_field | Required field absent (items array missing, or unit_cost_cents omitted from a line item) |
400 | invalid_field | A field has an invalid type or value (bad vendor_id, fractional unit_cost_cents, part_id not a positive integer, ticket_id not a positive integer, supplied part_id does not match an active product, items: [] on the receive endpoint, or location_id references an inactive location) |
400 | invalid_query | A list filter parameter was supplied as an array or object instead of a single scalar value |
400 | invalid_cursor | cursor is malformed |
400 | invalid_item | An element of the items array is not a valid object |
403 | insufficient_scope | API key lacks the required scope |
404 | not_found | Purchase order not found; vendor_id does not exist; or a line item’s ticket_id references a hidden or soft-deleted ticket |
422 | create_failed | The internal create operation failed (check message for details) |
422 | receive_failed | The internal receive/reconcile operation failed |
See Errors for the full error envelope format.