Tutorial 01
Quickstart: your first request
Generate an API key, verify it, and read your first page of inventory, in about five minutes.
Everything in this tutorial runs against the live API at https://api.stockpilot.dev. You need a Stockpilot account on the Growth tier or higher, because API access is gated to those plans.
Get a credential pair
Stockpilot authenticates with a static client ID and secret, not OAuth. In the Stockpilot app, open Settings → API and generate a pair. You get two values:
X-CLIENT-ID: 7c1f4ab93e5d2680
X-CLIENT-SECRET: e04b9d7a15c3f8621b0e94d7c5a3f8be
The secret is shown once. Store it the way you would store a database password.
Both headers go on every request. There is no token exchange, no refresh, and no Authorization header. Sending a bearer token instead will get you a 401.
Export them so the rest of this page is copy-pasteable:
export SP_CLIENT_ID="7c1f4ab93e5d2680"
export SP_CLIENT_SECRET="e04b9d7a15c3f8621b0e94d7c5a3f8be"
Verify the credentials
Before writing any integration code, confirm the pair works. /auth/who-is is the cheapest call in the API and tells you which organization you are acting as.
curl -s https://api.stockpilot.dev/auth/who-is \
-H "X-CLIENT-ID: $SP_CLIENT_ID" \
-H "X-CLIENT-SECRET: $SP_CLIENT_SECRET"
A working pair returns your organization:
{
"organization": "Acme Commerce BV",
"organization_id": 4812,
"plan": "growth"
}
If you get a 401, the pair is wrong or incomplete. If you get a 403, the credentials are valid but the plan does not include API access.
Read the rate-limit headers
Every response carries the current state of your budget. Ask for the headers on that same call:
curl -sI https://api.stockpilot.dev/auth/who-is \
-H "X-CLIENT-ID: $SP_CLIENT_ID" \
-H "X-CLIENT-SECRET: $SP_CLIENT_SECRET" | grep -i ratelimit
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 299
X-RateLimit-Reset: 47
X-RateLimit-Policy: read-single
These tell you which bucket the call fell into and how much of that budget is left. Pace your integration against them. See Rate limits and idempotency for the four buckets and what lands in each.
Read your first page of inventory
Now something real. List endpoints paginate with page and page_size:
curl -s "https://api.stockpilot.dev/inventory?page_size=5" \
-H "X-CLIENT-ID: $SP_CLIENT_ID" \
-H "X-CLIENT-SECRET: $SP_CLIENT_SECRET"
{
"count": 1284,
"next": true,
"previous": false,
"current_page": 1,
"total_pages": 13,
"results": [
{
"id": 34897,
"product_id": 456,
"item_name": "Vichy Homme Structure Force 50 ml",
"sku": "201156",
"barcode": "3337875647212",
"bin_location": ["A1-001-01", "B2-003-05"],
"threshold": "5u",
"stock_threshold": 5,
"quantity": 150,
"reserved_quantity": 3,
"incoming_quantity": 50,
"backorder_amount": 0,
"is_active": true
}
]
}
Three of those fields carry more than they look:
quantityis the physical count, including units already allocated to open orders.reserved_quantityis how many of those are spoken for.bin_locationis an array, because one item can sit in several bins.
Note there is no single “sellable” number here. What Stockpilot offers to your sales channels is computed across warehouses and arrives on the inventory.stock_changed webhook as offered_stock. The inventory model explains the difference and when each one is the right number.
next and previous are booleans here, not URLs. Page with current_page and total_pages, and note page_size caps at 100 on this endpoint. API conventions covers the two envelopes.
Prefer one paginated list call over many single lookups; they come out of different rate-limit buckets.
Sort and filter it
/inventory takes a sort, prefixed with - for descending, plus filters that combine:
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"
Sort on sku, item_name, barcode, quantity, base_price, stock_threshold, created_at or updated_at. Anything else is a 400.
Filter by supplier_id, brand_id, category_id, is_active, created_at and updated_after. An unknown supplier_id, brand_id or category_id is a 404 naming what was not found, rather than an empty page, so a typo does not read as “no results”.
Fetch a single item
When you already know an identifier, use the single-item endpoint. It accepts exactly one of id, sku or barcode. Sending two is an error.
curl -s "https://api.stockpilot.dev/inventory/get?sku=201156" \
-H "X-CLIENT-ID: $SP_CLIENT_ID" \
-H "X-CLIENT-SECRET: $SP_CLIENT_SECRET"
The same thing in Python
A minimal client you can build on:
import os
import httpx
client = httpx.Client(
base_url="https://api.stockpilot.dev",
headers={
"X-CLIENT-ID": os.environ["SP_CLIENT_ID"],
"X-CLIENT-SECRET": os.environ["SP_CLIENT_SECRET"],
},
timeout=30.0,
)
who = client.get("/auth/who-is").json()
print("authenticated as", who["organization"])
page = client.get("/inventory", params={"page_size": 5}).json()
for item in page["results"]:
print(f"{item['sku']:<12} {item['item_name'][:34]:<34} {item['quantity']:>5}")
Errors come back as JSON with a detail or error key, so a real client should check the status before parsing:
resp = client.get("/inventory/get", params={"sku": "DOES-NOT-EXIST"})
if resp.is_error:
raise RuntimeError(resp.json().get("detail", resp.text))
Where to go next
You now have a working credential pair and a client that can read. The three surfaces worth bookmarking:
- API reference covers every operation, rendered for humans.
- llms.txt is the same reference, compact, for the model in your editor.
- openapi.json is the raw schema, for generating a typed client.
The last two are regenerated from the schema on every request, so they cannot drift from the API.
