Create Purchase Order
POST /purchase_orders
🚀 Paid plan only
Creates a purchase order. The purchase_order envelope key is required,
and attributes outside the documented set are dropped SILENTLY rather
than rejected.
product_variant_id on a line item is the SELLABLE id (the same id the
sellables API returns) - no translation is needed. number is derived as
<company purchase order prefix>-<count + 1> when omitted; a supplied
value is accepted with no uniqueness check, so omit it.
estimated_delivery_days is recalculated from expected_date on any save
that changes that date, so send the date and leave the days alone. Totals
are computed on read rather than stored.
received_quantity on a line item is IGNORED on create - receiving only
happens on update.
Parameters
Section titled “ Parameters ”Header Parameters
Section titled “Header Parameters ”E.g. 123
Example
123Request Body
Section titled “Request Body ”object
object
Supplier ID. Required; must belong to the company.
Warehouse ID stock is delivered to. Required; must belong to the company.
Lifecycle state: draft -> active -> completed.
PO number. Auto-generated if blank.
Must be unique per company.
Expected delivery date (must fall between 2010-01-01 and 2035-01-01).
Default GBP. Cannot be changed once the PO is active.
Recalculated as whole days from created_at to expected_date on any save that changes either date, overwriting a value sent in the same request. Sent on its own it is stored as given. Send expected_date and omit this.
DEAD FIELD - it is stored and wired to nothing. Use send_supplier_report_email.
Emails the supplier the purchase order on save, but ONLY when the purchase order is (or becomes) active - a draft never emails, silently. Delivery is also suppressed entirely for some companies (multi-channel accounts, and sellers who have not used the standalone app) and the response looks identical, so never report it as sent.
NOT validated, so any string is accepted - deliberately left unconstrained here. Only pdf or csv attaches a document; every other value, and omitting it, sends the email with no attachment (unlike the reminder path, which defaults to pdf).
Line items.
object
Existing line item ID (update requests only).
Must exist in the company and not be deleted. Required for create requests.
Must be greater than 0. Required for create requests.
Unit cost. Must be greater than 0. Required for create requests.
Required for create requests.
Updates the supplier product variant reference.
Updates the supplier product variant title.
Update requests only — increments the received count for the line item. Cannot be negative.
Example
{ "purchase_order": { "supplier_id": 1748, "destination_warehouse_id": 1784, "expected_date": "2026-02-01T00:00:00Z", "currency_code": "GBP", "note": "Restock order", "shipping_and_handling": 12.5, "purchase_order_product_variants_attributes": [ { "product_variant_id": 1226617, "quantity": 10, "cost": 9.99, "tax_rate": 0.2 } ] }}Responses
Section titled “ Responses ”Created. Returns the new purchase order in the same shape as a List row and as Get Purchase Order.
object
object
Decimal serialized as a JSON string. Coerce before arithmetic. Defaults to 0. Set it explicitly for a purchase order in a currency other than the company’s: receiving multiplies the cost price by it.
object
Whole days from created_at to expected_date, recalculated on any save that changes either date. A value you send is kept only until expected_date next changes, so treat it as derived and drive it with expected_date.
The line items. This is the key - there is no line_items field on a purchase order.
object
Unit cost. Decimal serialized as a JSON string. Coerce before arithmetic.
Line item id. Pass it to the line-item sub-routes.
The SELLABLE id, the same id the sellables endpoints return. No translation is needed in either direction.
Units ordered.
Units received so far. Set indirectly by sending received_quantity as a delta, and never settable directly.
Always the purchase order’s supplier - a sent value is overwritten.
Decimal serialized as a JSON string. Coerce before arithmetic.
Decimal serialized as a JSON string. Coerce before arithmetic.
Decimal serialized as a JSON string. Coerce before arithmetic.
object
Decimal serialized as a JSON string. Coerce before arithmetic.
Decimal serialized as a JSON string. Coerce before arithmetic.
The same supplier shape as the purchase order’s own supplier.
object
object
object
object
object
object
object
object
Unique per company when present. A duplicate is a 400.
Decimal serialized as a JSON string. Coerce before arithmetic.
Only these three values are ever returned. The received-status values accepted by the list state filter are filter-only inputs.
Computed on read, never stored. Decimal serialized as a JSON string. Coerce before arithmetic.
object
The template used to render this supplier’s purchase order document, on a plan with customizable templates.
Reminder and report emails go here. Can be blank, and the send still reports success.
Write-only: there is no stored column, so List and Get always return null. A Create or Update response echoes the value sent in that same request.
Decimal serialized as a JSON string. Coerce before arithmetic.
Use this for the purchase order value, computed on read and never stored. Decimal serialized as a JSON string. Coerce before arithmetic.
Decimal serialized as a JSON string. Coerce before arithmetic.
Validation failed (missing supplier/warehouse, invalid line item cost/quantity/tax_rate, duplicate reference_number, or an out-of-range expected_date). Returns an array of error messages. On CREATE the line-item messages arrive unprefixed.
The company’s plan does not include purchase orders, or the user lacks the manage_purchase_orders permission. Body is {"error_messages":[...]}. Checked before the body is read.
A referenced record was not found, which is returned INSTEAD OF a 400: the supplier, the destination warehouse, or ANY product_variant_id is not in the company or is soft-deleted. The variant ids are checked as a whole set, so one bad id fails the entire request. Body is {"status":"404","error":"Not Found"}.