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.

·9 min read ·Updated 3 Sep 2026

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"
EndpointAccepted fields
/inventorysku, item_name, barcode, quantity, base_price, stock_threshold, created_at, updated_at
/purchase-orderscreated_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:

ValueMeaning
5uFive units
33wThirty 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.

StartHandlePoll
POST /shipping/request-labelentity_idPOST /shipping/retrieve-label
POST /purchase-orders/{order_id}/parcels/{parcel_id}/processentity_idGET /purchase-orders/{order_id}/parcels/{parcel_id}/status?entity_id=
POST /purchase-orders/recommendationstask_idGET /purchase-orders/recommendations/status/{task_id}
POST /sales-channels/sync-listingstask_idChannel 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-Key on 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 write bucket.
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.