Guide
Purchasing and inbound deliveries
Suppliers, forecast driven recommendations, and booking goods in with parcels. Includes the two fields that actually tell you whether stock arrived, and why status is not one of them.
Purchase orders track what you asked a supplier for and what has physically turned up. Those two things move independently, and most integration bugs here come from reading one as if it were the other.
Suppliers
GET /purchase-orders/suppliers/list lists who you can order from. Lead time and minimum order quantities are supplier properties, which is why recommendations are generated per supplier rather than across the whole catalogue.
Recommendations
Stockpilot already knows your sales velocity, lead times and inbound stock, so ask it rather than recomputing:
POST /purchase-orders/recommendations{
"supplier_id": 312,
"lead_time": 21,
"durability": 60,
"scope_days": 90,
"include_flagged": true,
"include_inbound": true
}
| Field | Meaning |
|---|---|
lead_time | Days between raising the order and stock landing |
durability | Days of cover the resulting order should provide |
scope_days | How much sales history to base the forecast on |
include_flagged | Include products flagged for review |
include_inbound | Subtract stock already on its way |
You get a task_id, not results. Poll GET /purchase-orders/recommendations/status/{task_id}
until it reports COMPLETED or FAILED, backing off between attempts. This route is in the heavy rate limit bucket at 20 per minute.
Raising the order
POST /purchase-orders accepts lines identified by product id, SKU or barcode, and mixed identifier types in the same request. That last part matters in practice: a warehouse app can send whatever a scanner produced without normalising first.
delivery_warehouse_id chooses where the goods land. Omit it and the default warehouse is used, which is also where inbound stock is counted.
Creating a purchase order is not idempotent. On an ambiguous failure, list purchase orders and check whether it already landed before resending.
Reading delivery progress
This is the part worth getting right.
GET /purchase-orders
carries total_delivered, total_remaining and fully_delivered on every row, which is enough to render a progress bar without a detail fetch per order.
status is not a delivery signal. A fully delivered purchase order stays ORDERED until someone explicitly marks it delivered in the Stockpilot UI, because marking it delivered has accounting side effects.
To tell whether goods arrived, read totals.fully_delivered and totals.total_remaining. Never status.
delivered_date follows status too, so it stays null on a fully delivered order until a human marks it delivered.
On GET /purchase-orders/{order_id} , each line carries four quantities:
| Field | Meaning |
|---|---|
delivered_quantity | Cumulative quantity actually delivered into stock |
remaining_quantity | Still outstanding, ordered minus delivered, floored at zero |
dispatched_quantity | Claimed by an in-flight parcel, not yet applied to stock |
invoiced_quantity | Quantity on the supplier invoice |
inbound_tracking is an object, not a list. It rolls the order up into total_ordered, total_delivered, total_remaining and total_parcels, then carries an items array per product and a parcels array of every delivery registered. inbound_tracking.parcels is where you get a parcel_id.
Booking in a delivery
POST /purchase-orders/{order_id}/parcels/createAsynchronous by default, returning 202. A 202 means the parcel was queued and the stock has not moved yet: applied is false, delivered_quantity is still the pre-parcel number, and the projected_delivered_quantity and projected_remaining_quantity fields are optimistic UI values rather than facts.
Do not poll parcel.status. It stays PROCESSING and is not a completion signal. Re-fetch GET /purchase-orders/{order_id} instead and read the confirmed quantities.
Pass "async": false and the request blocks until stock has actually moved, returning 200 with applied: true and real quantities. That mode runs backorder allocation, warehouse writes and an external accounting sync inline, so it is slow. Prefer async for large orders.
The flow that works:
GET /purchase-orders/{order_id}and pre-fill the parcel inputs fromremaining_quantity.POST .../parcels/createwith the adjusted quantities.- On
202, show theprojected_*numbers in a processing state. - Re-fetch the purchase order a few seconds later for confirmed
delivered_quantity. fully_deliveredturnstrueonce nothing is outstanding.
Delivering goods does not change the purchase order’s status. See above.
When a parcel is stuck
A non-zero dispatched_quantity means a parcel is mid-flight. If it never clears, the parcel is stuck, and POST /purchase-orders/{order_id}/parcels/{parcel_id}/delete
releases it.
Do not reach for this on every 409. A 409 from parcels/create normally means a parcel really is in flight, quite possibly one a warehouse user started in the UI seconds ago. Removing it throws away their work.
- On
409, noteblocking_parcel_id, wait a few seconds, re-fetch the purchase order. - If
delivered_quantitymoved, the parcel completed. Nothing to remove. - If the same
dispatched_quantityvalues are still there well after the fact, the parcel never completed. Only then delete it. - Re-create the delivery.
Two more things about deletion. It does not reverse stock: only quantities never applied are released, anything a completed parcel already delivered stays delivered, and a COMPLETED parcel is refused with a 409. And the release is scoped to the purchase order, not the parcel: every line with a non-zero dispatched_quantity is reset, whichever parcel claimed it. In practice only one parcel is in flight at a time, so it is the same set, but do not use this to prune one of several pending parcels.
Removal is permanent. The parcel is deleted rather than marked failed, so it disappears from total_parcels and from the order’s parcel history.
