Skip to content
Knowledge ERP Docs

API Reference ↗
Purchasing

Purchase Orders

A purchase order records a request to procure goods from a vendor and tracks the full lifecycle from draft through line-item receiving to vendor invoice creation.

A purchase order (PO) is the formal record of a request to buy goods from a vendor. It holds the vendor, the receiving location, an ordered date, an expected delivery date, and one or more line items — each pairing a Product with a quantity and unit cost. When units arrive, you receive them against those lines and the resulting inventory units land directly in stock. After receiving, a vendor invoice can be generated from the PO in a single step.

POs connect purchasing to inventory. Receiving a line creates real inventory units and writes a Purchase movement on each one, so your stock levels and movement history are always accurate.

Creating a purchase order #

New purchase orders are created from Purchasing → Purchase Orders → New Purchase Order.

  • Vendor — the supplier you are ordering from. Required. Pulling up a vendor also surfaces any vendor SKU links you have recorded, which the line-item form can draw on for default costs.
  • PO Number — auto-generated as PO-0001, PO-0002, … if left blank, or you can type your own.
  • Status — defaults to Draft; see the lifecycle section below.
  • Receiving Location — which Location the stock should land in. Required: every line on a purchase order is a Product that has to physically arrive somewhere, and an order naming no Location is not counted as on order by the Reorder Queue, so you would be told to re-order something already coming. If you have one Location it is filled in for you and the field stays hidden; with several you are asked. It also sets the initial receiving bin on the receive page.
  • Order Date and Expected Delivery — informational dates shown in the list and on the PDF. A purchase order raised with Create Purchase Order on the Reorder Queue or the Low Stock report fills in Expected Delivery as today at its Location plus the lead time set for that product from that vendor.
  • Notes — free-text field visible on the PO PDF sent to the vendor.
  • Tax Rate — an optional tax rate applied to the subtotal; the resulting tax_amount and total_amount are recomputed automatically whenever the rate or any line item changes.
  • Custom fields — any extra attributes defined for purchase orders appear at the bottom of the form.

Line items #

After saving the PO header, add lines on the Items tab (or in the relation manager on the edit page).

Each line captures:

  • Product — which product you are ordering. Required.
  • Vendor Part # — the vendor's own part number for this line. Auto-filled from the Product's vendor record for this PO's vendor when you pick the Product, but freely editable per line and printed on the PO PDF sent to the vendor.
  • Quantity Ordered — how many units you expect.
  • Unit Cost — what you are paying per unit, in the order's currency (see Currency). Stored and returned as an integer in the currency's smallest unit, such as cents (5000 = $50.00).
  • Notes — optional per-line notes.

The PO's total amount refreshes every time a line or a charge is added, edited, or deleted: it sums quantity_ordered × unit_cost across all lines, adds the tax amount if a tax rate is set, then adds the charges.

Currency #

A purchase order, vendor bill or vendor return is in its vendor's currency: your home currency, US dollars (USD), unless the vendor is set to another one. Its amounts are entered and shown in that currency. A document's currency is set when it is made and can't be changed afterwards, and a payment is in its bill's currency.

The exchange rate #

A document in another currency carries an exchange rate: how many US dollars one unit of its currency is worth. It is filled in from the European Central Bank's (ECB) rate for the document's date, or the last one before it, when you make it. The Exchange rate field on the purchase order and bill forms shows the ECB's rate and its date, and you can type your own instead.

  • A rate more than 10% away from the ECB's is refused until you tick Use this rate even though it is more than 10% from the ECB's: it is more often an inverted rate or a missing digit than a real price.
  • For a currency the ECB doesn't publish, there is no rate to fill in. Type one before the purchase order is submitted or the bill is sent for approval.
  • Clearing the rate puts back the ECB's.
  • A bill's rate is fixed once it is approved. A purchase order's rate is locked with its total while it waits for approval, once it is approved, or once it has been sent: withdraw it to Draft, or unlock it, to change the rate. Once anything on it is received, the rate is fixed for good.

Limits, approvals and totals #

Purchase limits, approval limits, approval rules on the total and a bill's cash match tolerance are in US dollars, and a document in another currency is weighed in dollars. In a currency the ECB publishes, it is weighed at the higher of its own rate and the ECB's stored rate nearest its date, so typing a low rate can't bring it under a limit or widen a bill's tolerance. In a currency the ECB doesn't publish, there is no ECB rate to compare with, so the rate typed is what it is weighed at: an order in such a currency always needs approval, the approver is shown the rate, and whoever typed it can't approve the order or bill while somebody else could. Such a bill has no cash match tolerance, only the percentage one.

  • An order in a currency the ECB doesn't publish always needs approval, however large the submitter's purchase limit, and the approver is told the rate it was entered at.
  • Whoever typed a document's rate can't approve it while somebody else could, just as whoever raised it can't. So when one person raised it and the only other person who could approve it typed its rate, neither can: have somebody else enter the rate or, in a currency the ECB publishes, clear it to use the ECB's (withdraw a purchase order to Draft first). Somebody who is the only person able to approve it still can.

Lists, reports and the dashboards that add up documents in more than one currency add them in US dollars, each at its own rate.

Over the API #

A purchase order, vendor invoice, vendor return and vendor payment each return four fields about currency: currency, exchange_rate, rate_date and rate_source (ECB, manual for a typed rate, or identity for your home currency, whose rate is always 1). Send exchange_rate when you create or update a purchase order, vendor invoice or vendor return in another currency, and exchange_rate_confirmed: true to keep one more than 10% from the ECB's. In your home currency a rate you send is not kept. A payment's rate is returned only.

A purchase order also returns total_amount_home: its total in your home currency, in cents, at its rate. For an order in your home currency it is the same as total_amount.

Charges #

Freight, duty, and anything else you expect to pay on top of the goods go on the Charges tab of the PO's edit page. Each charge has a type (Freight, Duty, or Other), an optional description, and an amount, in dollars in the form and in cents over the API (5000 = $50.00), in your home currency.

Charges count toward the order's total, so they count toward your purchase limit and toward committed spend: a $400 order that is inside a $500 limit needs approval once $150 of freight is added to it.

Charges are not taxed, with one exception you can turn on. A Location can be set to Tax freight and shipping charges on purchases. Where it is on, the charges of type Freight on that Location's orders are taxed at the order's tax rate, together with the lines, and the tax shows in the order total and in the figure your purchase limit is checked against. Duty and Other charges are never taxed. The setting is read while an order is a draft; an order that is waiting for approval, approved or sent keeps the tax it already has when the setting changes. Only someone who may update purchase orders at the Location can change it.

Charges become part of what the stock cost. When goods are received, each unit's cost is the line's unit cost plus its share of the order's charges (its landed cost). A charge is spread over the order's lines by their ordered value, so a line worth half the order takes half the freight. Each unit received then takes its part of the line's share in proportion to its quantity, so a line received in several deliveries ends with exactly its share. A line too small to make a whole unit has its share, expected or billed, recorded as a variance, and so does a line whose order is closed before it made one. If a bill for the order has been approved with its own charges of a type, such as the freight the carrier actually billed, a delivery takes its share of those rather than the order's expected ones, less what the line's earlier deliveries already took for that type. Each delivery after a bill's approval is then trued up as below, so the line ends on exactly its share of the bill, larger or smaller. A delivery takes its share of an approved bill's charges even when the bill itself is hidden from you.

A unit that carries landed cost can't be permanently deleted, and neither can its order, line or charge, because the landed-cost records are kept as the unit's cost history. Each can still be deleted the ordinary way, which can be undone.

When the bill is approved, the stock is trued up to what was actually charged. A bill's charges replace the order's expected charges of the same type, and are spread over everything received so far by its received value. A line that has received goods but has no units to carry its share, such as one too small to make a whole unit, takes its share as a variance. The difference from what each unit already carries is then booked:

  • Units still in stock take it into their cost, even if part of a unit has been used.
  • Units already gone (sold, consumed or discarded) keep the cost they left with, and the difference is recorded as a variance.

A charge on the bill that the order never expected is spread the same way. Two approved bills charging the same type, such as two freight bills, are added together. If the bill stops being approved, for example because it is moved back to draft or deleted, its charges stop counting. For a charge type no other approved bill covers, the order's expected charges apply again: every unit received, including those received while the bill stood, is brought to its share of them by quantity, with the difference booked as above. A line that will never have a unit (too small to make one, or on a closed order) goes back to its expected share for all it received, as a variance. A carrier's bill works the same way when its charge names the order.

Deleting a unit lowers its cost by its landed cost. The next delivery on its line does not take that unit's share again, and restoring the unit brings its share back. If a bill for the order has been approved, the line still ends on the bill: the deleted unit's share of it is spread over the units left, and spread again when the unit is restored. Every one of these amounts is recorded and never edited: a change is a new entry.

Where a variance goes #

A variance here is a share of a charge that doesn't go into any unit's cost (not the bill's Approve Variance action, which is about a bill that doesn't match its order): the unit had already gone, or the line never made a unit. It is kept in the order line's landed-cost record, as an entry that is written once and never edited. Variances aren't listed on a screen or a report today. What you see is their effect: a unit's Cost and its Landed cost from list leave a variance out. QuickBooks is sent the bill's amount due as one figure, so a variance isn't sent on its own.

Like the lines, charges are frozen while the PO is waiting for approval, once it is approved, and once it is sent to the vendor. Adding, changing the amount of, or removing one, or changing its type, is refused until the order is withdrawn or unlocked. Changing a charge's description is not. A charge on an approved bill can't have its type or its order changed.

The purchase order lifecycle #

A PO moves through five statuses:

  • Draft — editable; not yet sent to the vendor.
  • Submitted — sent to the vendor. The Submit to Vendor action on the view page marks the PO submitted and, if the vendor has an email address, queues an email with a PDF attachment, branded as your business — the subject reads "Purchase Order PO-0007 from Tallgrass Outfitters" (your business name) and the sender name is yours, so the vendor recognizes who is ordering. Submitting is subject to spend approval: a PO whose approval status is Pending is refused, and no email goes out.
  • Partially Received — at least one line item has stock received against it but not all lines are fully received. Status advances automatically when you receive the first unit.
  • Received — nothing more is expected on the PO. Status advances automatically when the last receipt brings every line's quantity_received up to quantity_ordered. A Partially Received PO can also be closed early with Close PO when the vendor won't ship the rest; it then shows as Received too, with some lines short.
  • Cancelled — the PO is closed with no further receiving allowed.

You can also Print or Download PDF from the view page at any status — useful for sending the PO manually or keeping a paper record. Print opens the PO with your browser's print dialogue over it; Download PDF saves the file.

Spend approval #

A PO carries an approval status alongside its lifecycle status, so an order can be (say) Approved and Partially Received at the same time. It is one of Not Required, Pending, Approved, or Rejected, and only Not Required or Approved may be sent to the vendor.

Two per-person limits, both in cents, decide what happens on submit:

  • Purchase limit — what that person may commit on their own. Empty means every order they raise waits for approval. The person who signs an account up starts with it empty, so on a new account an order goes to the vendor only once it is approved, until you give the buyer a limit.
  • Purchase approval limit — what that person may approve for others. 0 means no approval authority at all, which is what every existing user starts with; empty means unlimited. For a service account (the user an integration's API key acts as), empty means it cannot approve: it approves only up to a figure you set, even if it holds the Admin role.

An administrator can approve an order of any amount, whatever their own approval limit, so on a new account the administrator can approve its orders. Like anyone else, an administrator does not approve their own orders unless nobody else can (see Approving and rejecting below). To let a buyer place orders without waiting, give them a purchase limit on their user.

When you press Submit to Vendor, the total is recomputed from the order's lines and charges. Within your purchase limit, the PO submits with approval status Not Required. Above it, the PO becomes Pending and is not sent to the vendor.

An order an approval rule has marked goes to the approvers whatever your limit. An automation rule with a Require Approval action marks the order as needing approval — as the order is created, or as it is submitted, depending on the rule's trigger. The order stays an editable Draft until it is submitted; then it becomes Pending. The mark stays on the order, so one that is withdrawn or unlocked and submitted again goes back to the approvers.

An order that was rejected is the exception. However large your own purchase limit, submitting it does not send it: it goes back to Pending for an approver to look at again. Your limit governs orders nobody has turned down — it is not a way round somebody's decision.

Approving and rejecting #

A pending PO can be actioned by any user who holds the approve_purchase_orders permission and whose purchase approval limit covers the total. There is no supervisor chain: approvers are a pool, not a tree.

A role held at one location approves that location's orders. Someone whose Approve Purchase Orders permission comes from a role granted at a particular location can approve — and reject — orders for that location, and not orders for another. Someone who holds the permission account-wide approves anywhere, as before. An order that names no location can only be approved by an account-wide holder — granting the permission at a location does nothing for such an order. Orders you raise on the purchase order screen, through the API, from the Reorder Queue, from the Low Stock report or from a reorder point all name one. An order created by an automation rule does not, because that rule is never asked for a location.

Except whoever raised the order. You cannot approve your own purchase order while somebody else could approve it instead — the same rule vendor bills have always followed. If nobody else can, you can: a one-person business is not a segregation-of-duties problem, and refusing there would leave you unable to buy anything.

  • Approve records who approved it and when; the PO can then be submitted.

  • Reject requires a reason and returns the PO to an editable Draft, so the buyer can put right whatever was wrong with it. The rejection — who, when, and why — stays on the record permanently, so the history of a turned-down order is never erased.

    Sending it again needs an approver, whatever the buyer's own purchase limit. Pressing Submit to Vendor on a rejected order puts it back to Pending rather than sending it, and one of the approvers clears it from there. The rejection and its reason stay visible to whoever picks it up.

Approving and rejecting also work over the API and through a connected AI assistant, including with an integration's API key. They ask the same things the Approve button does: the permission at the order's location, an approval limit that covers the total, and the rule about approving an order you raised. The order records how each decision arrived, shown as Approved Through or Rejected Through: the app, an assistant connector, or the name of the API key.

A decision is for the total and vendor the approver saw. If either has changed since the Approve or Reject dialog opened — the order was withdrawn, changed and submitted again meanwhile — confirming is refused and the order stays waiting, so the approver can look again. Over the API and through an assistant, send the total you are deciding on, in the order currency's minor unit, as expected_total, and the vendor's id as expected_vendor_id; a different one is then refused with a 409. Without them, the decision is made on the order as it is at that moment, so integrations should always send both:

curl -X POST https://your-domain.com/api/v1/purchasing/purchase-orders/<id>/approve \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"expected_total": 501000, "expected_vendor_id": "<vendor-id>"}'

If nobody can approve it — not a colleague, and not you — the PO shows a warning naming the amount, and saying what to check: who holds Approve Purchase Orders at the order's location, and their purchase approval limits. The order is never silently approved and never left looking ordinary. What clears it depends on which it is — raise an approver's limit, or grant Approve Purchase Orders at that location. If neither seems to apply, check whether somebody who looks like they hold the permission has had it removed on their own record, which overrides anything their roles grant: it shows as Removed for this person only on their entry under People, and lifting it there is what restores them. On an order naming no location the warning says so instead, because only an account-wide holder can approve that one. You will not see this warning merely because you raised the order yourself: if you are the only person who could clear it, you still can.

A pending or approved PO is locked #

An approval authorizes a specific commitment: this much money, to this vendor. So while a PO is Pending — waiting for an approver — and once it is Approved, that commitment is frozen: approving $900 when the order has since become $9,000 would be an approval nobody actually gave. It can't change while it waits for approval, and a decision made on an outdated screen is refused — see Approving and rejecting.

Locked — the vendor, the tax rate, the line items, and the charges. Lines cannot be added, edited, removed, or moved between orders, and a charge cannot be added, removed, or have its amount changed.

Still editable — notes, the PO number, order and expected-delivery dates, the receiving location, and custom fields. A buyer chasing a late delivery should not need a fresh approval to write down a new date. Receiving stock against the order is unaffected.

This is not a setting, and there is nothing to switch on. Some systems ship the equivalent lock turned off by default, which means out of the box a confirmed order's amount can be raised with nobody re-approving it.

Withdraw to Draft takes a Pending PO back from its approvers so it can be changed. A reason is optional. The order returns to an editable Draft, and who withdrew it, when, how, and why are kept on it. Submitting it again is checked like any other submission.

Unlock for Editing re-opens an approved or submitted PO: it asks why, discards the approval, and returns the order to Draft, to be submitted again. Submitting it again checks the new total against the submitter's purchase limit, or sends it to the approvers if an approval rule marked it. Who unlocked it, when, and why are kept on the order. It is available while the PO is a Draft or Submitted, and not once stock has been received against it.

Submitting, withdrawing and unlocking are not undone or redone from the change history: use these actions.

Withdrawing and unlocking need the update purchase orders permission — the permission to edit an order, since that is what they are for — held either account-wide or at the order's own location. Anyone with it may withdraw an order, not only whoever raised it. Withdrawing also works over the API and through a connected AI assistant:

curl -X POST https://your-domain.com/api/v1/purchasing/purchase-orders/<id>/withdraw \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Vendor re-quoted the freight."}'

Neither needs approve_purchase_orders: the person fixing a line is usually the buyer, not an approver. Unlocking cannot spend anything (it removes the approval, and a resubmission is checked like any other submission), but it does discard an approval someone else gave, so it is not open to everyone.

Receiving stock #

Click Receive Items on the PO's view page (visible for Submitted and Partially Received POs) to open the dedicated receive screen. There is one receiving screen and this is it — the Receive button beside an individual line on the Line Items tab opens the same screen with that line already selected, rather than a second form of its own.

Receiving needs the receive purchase orders permission, held either account-wide or at the order's own location — so a warehouse hand whose role covers one location can sign for deliveries there. Opening another location's order is refused, and so is sending the stock to a bin belonging to a location their role does not cover.

The receive screen shows each line with its ordered and remaining quantities. For each line you want to receive:

  1. Select the line — click it in the list, or scan a Product barcode to jump straight to the matching line.
  2. Set the destination — choose the location and bin where the stock should land. The screen pre-fills the location's default receiving bin — or, if that location has none set, wherever the Product was last put away. You can also scan a bin barcode to switch destinations on the fly. If the account has switched Bins off in Inventory settings, there is no bin to choose: the stock lands in the chosen location's default receiving bin.
  3. Enter quantity — defaults to the remaining quantity on the line.
  4. Optionally enter a lot number and expiration date if the Product's category tracks them.
  5. Click Receive (or press Enter).

Each receipt creates the whole inventory units the line's received total gains, and writes a Purchase movement against each, carrying that unit's share of the receipt — so the movements for one receipt add up to the quantity received, not to the quantity multiplied by the number of units. A line of 3 received as 2.6 and then 0.4 gets 3 units. A receipt that brings the line no whole unit, such as a first 0.4, creates none: it is still recorded on the line, and the Movement Log shows it as a Purchase with no unit. The line's quantity_received increments and the PO status updates automatically. A scrollable Recent Receipts log shows the last five receipts so you can catch mistakes quickly.

Receiving over the API or through a connected AI assistant asks the same things this screen does: the receive permission at the order's location, a bin at a location where you may receive, a quantity above zero, with at most 4 decimal places and within what is left, and one serial number per unit for a product that tracks them. Several lines in one request are received together or not at all: if any line is refused, nothing is booked, and the reply names the line and the reason.

Creating a vendor invoice from a PO #

Once a PO reaches Received status, the Record Invoice action appears on the view page. Clicking it opens a small form where you confirm the vendor's invoice number, invoice date (today at the PO's Location to start with), due date, and amount due (pre-filled from the PO total). Saving creates a vendor invoice with one line per PO line — quantities and costs are carried over automatically — and takes you straight to the new invoice.

The purchase order list #

Purchasing → Purchase Orders shows each PO with its PO number, vendor, status badge, ordered date, expected delivery date, and total. Filter by status (multi-select), Product (finds any PO whose lines include that Product), or use the Trashed filter to view soft-deleted orders.

What lives on the PO page #

Open a PO to see its header infolist and tabs for everything attached to it:

  • Line Items — the line items; new lines cannot be added once the PO is fully received.
  • Received Inventory — every inventory unit created by receiving against this PO, with its current location and status.
  • Vendor Invoices — vendor invoices linked to this PO.
  • Attachments — any files uploaded against the PO.
  • Changes — a full audit trail of edits.

Doing it from the API #

# Create a purchase order
curl -X POST "https://your-domain.com/api/v1/purchasing/purchase-orders" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "vendor_id": "...",
    "ordered_at": "2026-06-02",
    "expected_at": "2026-06-16",
    "notes": "Please ship via ground freight."
  }'

# Add a line item (unit_cost in the order currency's minor unit: cents for USD)
curl -X POST "https://your-domain.com/api/v1/purchase-order-items" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "purchase_order_id": "...",
    "product_id": "...",
    "quantity_ordered": 10,
    "unit_cost": 4500
  }'

# Add an expected charge (amount in the order currency's minor unit: 5000 = $50.00 in USD)
curl -X POST "https://your-domain.com/api/v1/purchase-order-charges" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "purchase_order_id": "...",
    "type": "freight",
    "amount": 5000
  }'

# List POs for a specific vendor
curl "https://your-domain.com/api/v1/purchasing/purchase-orders?vendor_id=..." \
  -H "Authorization: Bearer $TOKEN"

# Submit a draft to its vendor
curl -X POST "https://your-domain.com/api/v1/purchasing/purchase-orders/{id}/submit" \
  -H "Authorization: Bearer $TOKEN"

# Approve, or reject with a reason, an order awaiting approval
curl -X POST "https://your-domain.com/api/v1/purchasing/purchase-orders/{id}/approve" \
  -H "Authorization: Bearer $TOKEN"
curl -X POST "https://your-domain.com/api/v1/purchasing/purchase-orders/{id}/reject" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Wrong vendor for this part."}'

# Receive two lines, each into a bin (serial_numbers only for products that track them)
curl -X POST "https://your-domain.com/api/v1/purchasing/purchase-orders/{id}/receive" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: delivery-2291" \
  -d '{"lines": [
        {"line_id": "...", "quantity": 10, "bin_id": "...", "lot_number": "L-2291"},
        {"line_id": "...", "quantity": 2, "bin_id": "...", "serial_numbers": ["SN-1", "SN-2"]}
      ]}'

Submitting over the API works as Submit to Vendor does: an order over your purchase limit, a rejected one, or one an approval rule holds goes to the approvers instead of the vendor. The response's meta.outcome is submitted or pending_approval, and meta.emailed says whether the vendor was emailed. A refused approval or rejection answers 403 with the reason, such as the order being above your approval limit; one for an order that is not awaiting approval answers 422.

Send an Idempotency-Key with a receipt if your integration might retry it, for example after a timeout. Use any unique value, up to 255 characters, and send the same one on every retry of that receipt. A retry with the same key and the same lines gets the first response back, marked Idempotent-Replayed: true, and books nothing again. The same key with different lines is refused with a 422. A key lasts 24 hours and belongs to the person whose API key sent it. A refused receipt doesn't use up its key, so you can correct it and send it again with the same key. Without a key, every request books. Through a connected AI assistant, the same value is the receive tool's idempotency_key.

Money fields are integers over the API, in the order's currency and its smallest unit — unit_cost: 4500 means $45.00 on a USD order, and total_amount is returned the same way; see Currency.