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.

·10 min read ·Updated 3 Sep 2026

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
}
FieldMeaning
lead_timeDays between raising the order and stock landing
durabilityDays of cover the resulting order should provide
scope_daysHow much sales history to base the forecast on
include_flaggedInclude products flagged for review
include_inboundSubtract 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:

FieldMeaning
delivered_quantityCumulative quantity actually delivered into stock
remaining_quantityStill outstanding, ordered minus delivered, floored at zero
dispatched_quantityClaimed by an in-flight parcel, not yet applied to stock
invoiced_quantityQuantity 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/create

Asynchronous 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:

  1. GET /purchase-orders/{order_id} and pre-fill the parcel inputs from remaining_quantity.
  2. POST .../parcels/create with the adjusted quantities.
  3. On 202, show the projected_* numbers in a processing state.
  4. Re-fetch the purchase order a few seconds later for confirmed delivered_quantity.
  5. fully_delivered turns true once 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.

  1. On 409, note blocking_parcel_id, wait a few seconds, re-fetch the purchase order.
  2. If delivered_quantity moved, the parcel completed. Nothing to remove.
  3. If the same dispatched_quantity values are still there well after the fact, the parcel never completed. Only then delete it.
  4. 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.

A Stockpilot engineer talking through an integration with a developer

Stuck on something

Ask a person, not a search box

The API surface is wide, and some of it only makes sense once someone explains why it works that way. If a payload is not doing what you expect, or you are weighing two approaches, say so and we will look at it with you.