Guide
API conventions
The two pagination envelopes, sorting, dates, identifiers, location and threshold formats, error shapes and async operations. The rules that apply across every endpoint.
These rules hold across the whole API.
Pagination
Every list endpoint takes page and page_size. page starts at 1, page_size defaults to 100.
curl -s "https://api.stockpilot.dev/inventory?page=2&page_size=500" \
-H "X-CLIENT-ID: $SP_CLIENT_ID" \
-H "X-CLIENT-SECRET: $SP_CLIENT_SECRET"
There are two envelopes, and they page differently. Write one paginator against the wrong one and it will silently read page one forever.
Counted envelope, on /inventory, /assemblies, /purchase-orders and /returns. next and previous are booleans, not URLs:
{
"count": 2150,
"next": true,
"previous": false,
"current_page": 1,
"total_pages": 22,
"results": [ ]
}
Page until current_page == total_pages. Requesting a page beyond total_pages returns 404, so read total_pages from the first response rather than probing for the end.
URL envelope, on /products and /orders. next carries the URL of the following page, or null:
{ "count": 250, "next": "https://api.stockpilot.dev/products?page=2", "previous": null, "results": [ ] }
A paginator that works on both reads neither field:
def all_pages(client, path, page_size=100, **params):
page = 1
while True:
data = client.get(path, params={**params, "page": page, "page_size": page_size}).json()
rows = data["results"]
yield from rows
if len(rows) < page_size:
return
page += 1
page_size caps at 1000 on most endpoints, but at 100 on /inventory, /assemblies, /purchase-orders and /returns.
Sorting
/inventory and /purchase-orders take a sort parameter. Prefix a field with - for descending, so -quantity puts the largest stock first.
curl -s "https://api.stockpilot.dev/inventory?supplier_id=12&is_active=true&sort=-quantity" \
-H "X-CLIENT-ID: $SP_CLIENT_ID" -H "X-CLIENT-SECRET: $SP_CLIENT_SECRET"
| Endpoint | Accepted fields |
|---|---|
/inventory | sku, item_name, barcode, quantity, base_price, stock_threshold, created_at, updated_at |
/purchase-orders | created_at, updated_at, expected_date, delivered_date, order_total, status, cart_id, supplier |
Anything outside those sets is a 400, not a silently ignored parameter.
With ?status=DRAFT the set is narrower: only created_at, updated_at, order_total, cart_id and supplier. The other three fields are not set on a draft, so sorting by them is a 400.
Sorts are tie-broken on the record ID, so a paginated sweep over a sorted list will not duplicate or skip rows.
Identifying a product
Endpoints that look up a single product accept exactly one of id, sku or barcode.
curl -s "https://api.stockpilot.dev/inventory/get?sku=201156" # good
curl -s "https://api.stockpilot.dev/inventory/get?id=34897" # good
curl -s "https://api.stockpilot.dev/inventory/get?sku=201156&id=34897" # error
Passing none or more than one is a client error, not a silent preference for whichever came first.
Dates and times
Dates are ISO YYYY-MM-DD. Timestamps are ISO 8601 with a timezone offset.
2026-09-03
2026-09-03T09:14:22.481Z
Analytics endpoints take from and to as plain dates. Webhook envelopes carry event_triggered_at as a full timestamp.
Locations
Warehouse locations are hierarchical bins, written as aisle, rack, shelf:
A1-001-01
The format is a string, not a structured object. Match on it literally rather than parsing it into parts unless you know how a specific warehouse is laid out.
Thresholds
A reorder threshold is either a number of units or a number of weeks of cover:
| Value | Meaning |
|---|---|
5u | Five units |
33w | Thirty three weeks of stock cover, based on measured demand |
The weeks form is what makes a fast selling SKU trigger long before a slow one at the same unit count.
Order statuses
open, pending, on-hold, completed, cancelled
Filter list calls with ?status=open. Note that the Stockpilot CLI’s status command reports on open orders specifically.
Errors
Every error returns JSON with a detail or an error key, alongside the HTTP status.
{ "detail": "Product not found for sku 'DOES-NOT-EXIST'" }
A robust client checks the status before parsing:
resp = client.get("/inventory/get", params={"sku": sku})
if resp.is_error:
raise StockpilotError(resp.status_code, resp.json().get("detail", resp.text))
Asynchronous operations
Several operations are too slow to answer inline. They all follow the same start then poll shape: the start call returns a handle rather than a result.
| Start | Handle | Poll |
|---|---|---|
POST /shipping/request-label | entity_id | POST /shipping/retrieve-label |
POST /purchase-orders/{order_id}/parcels/{parcel_id}/process | entity_id | GET /purchase-orders/{order_id}/parcels/{parcel_id}/status?entity_id= |
POST /purchase-orders/recommendations | task_id | GET /purchase-orders/recommendations/status/{task_id} |
POST /sales-channels/sync-listings | task_id | Channel state via GET /sales-channels |
Persist the handle before doing anything else. It is the only way to ask what happened.
Back off between polls. All of these sit in the heavy rate limit bucket at 20 requests per minute.
What is not there
Worth knowing so you do not go looking:
Idempotency-Keyon three endpoints only, the two assembly builds and supplier create. Which other calls are safe to retry is covered in rate limits and idempotency.- No cursor pagination. Offset paging only.
- No field selection or sparse responses. Endpoints return their full shape.
- No bulk write endpoints. Loop, and pace yourself against the
writebucket.
