Welcome to the StockIQ Public API reference. These endpoints let your integrations pull planning data — forecasts, demand history, sales and supply orders, item-site attributes, and safety stock levels — directly from your StockIQ instance for use in tools such as Excel, Power BI, or your own applications.
Individual Endpoint Information can be found in these documents:
You can give the JSON file to an AI of your choice to help generate an integration to StockIQ's Public API
Before Starting
Before you are able to access StockIQ's Public API, an administrator must create an API token in StockIQ by following these steps
- Sign in to StockIQ and go to Admin → Manage Security.
- Select the API Tokens tab.
- Click Add.
- Fill in the token details:
- Name- a label you will recognize later, for example "Acme ERP – Nightly Sync". This name is what appears in StockIQ's logs and change history, so make it specific. Names must be unique.
- Description- optional free text; useful for recording who asked for it and why.
- Expires- when the token stops being accepted. We strongly recommend setting one. Leave it blank only if you have a specific reason to create a token that never expires.
- Roles- the roles that determine what the token may do.
- Data scopes- optionally restrict the token to specific sites, suppliers, buyers, shippers or item categories, exactly as you would for a user.
- Save. StockIQ displays the token
IMPORTANT: The token is shown once, at the moment it is created. StockIQ stores only a one-way hash of it and cannot show it to you again. Copy it somewhere safe before closing the dialog. If you lose it, you must rotate the token to get a new one.
Once you have it, include the token on every request to StockIQ's public API as a bearer credential:
Authorization: Bearer <your-api-token>
Treat the token like a password. Don't commit it to source control, paste it into tickets or chat, or share one token across multiple integrations or machines.
Getting started
All requests are made against your instance's base URL (for example https://yourcompany.stockiqtech.net).
Step 1 — authenticate. Exchange the token secret for a short-lived bearer JWT by POSTing it to /api/authenticate (in the JSON body as shown, or in an X-Api-Key header if that is easier for your client):
POST /api/authenticate
Content-Type: application/json
{"token": "siq_XXXXXXXXXXXX"}The response contains the JWT and its lifetime in seconds:
{"access_token": "eyJhbGciOi...", "token_type": "Bearer", "expires_in": 3600}Step 2 — call the API. Send the JWT in the Authorization header of every request. An empty filter object {} means "no restriction", so this first request returns the first 1000 item-sites your token is allowed to see:
POST /api/ItemSiteDetail/fetchby/hierarchyNodeFilters?page=1&pageSize=1000
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json
{}Because that request asks for a page, its response is the paged envelope described under Paging below — the item-site records arrive under Data, not as a bare array.
Tokens expire after one hour. When a request comes back 401 Unauthorized, call /api/authenticate again and retry — there is no separate refresh flow. The token secret itself does not expire; store it like a password and never embed it in client-side code.
Filtering
Query endpoints accept a filter object in the request body. Every filter dimension can be supplied as numeric ids or as the business codes you already use in your ERP (ItemCodes, SiteCodes, PrimarySupplierCodes, ...), so no id lookup round-trip is needed. Empty or omitted filter lists mean "no restriction". A code that does not exist in your instance fails the request with a 400 listing the unresolved codes — requests never silently return broader data than you asked for. Dates (StartDate/EndDate) are ISO formatted: "2026-01-31".
Paging
Paging is opt-in on endpoints that support paging- and opting in changes the shape of the response. With neither page nor pageSize supplied, an endpoint returns the complete result set as a plain JSON array of records. Add either parameter to the query string and the same endpoint returns a paged envelope instead: the records move under Data, joined by the navigation fields. Code that reads the response has to expect whichever of the two forms it asked for.
No paging parameters — a bare array of records:
[ { "ItemSiteId": 1401, "ItemCode": "A-100", ... }, { "ItemSiteId": 1402, ... } ]The same request with ?page=1&pageSize=1000 — those records moved under Data, wrapped in the envelope:
{
"CurrentPage": 1,
"NextPage": 2,
"PreviousPage": null,
"PageSize": 1000,
"TotalPages": 7,
"TotalRecords": 6284,
"Data": [ { "ItemSiteId": 1401, "ItemCode": "A-100", ... }, { "ItemSiteId": 1402, ... } ]
}-
page— 1-based page to return (default 1) -
pageSize— rows per page (default 1000, maximum 10000)
To pull a full data set, re-send the same request with ?page=NextPage until NextPage comes back null (the Python sample below shows the loop). The pages of one walk are served from a consistent snapshot taken when the first page is requested, so rows do not shift between pages; the snapshot is kept for about 5 minutes after each page request (30 minutes at most), so finish a walk promptly rather than resuming it hours later. Requesting a page past the end returns an empty page whose TotalPages tells you where the data ends. Prefer paging whenever the row count can be large.
Two bulk-extraction endpoints are always paged — their row spaces are too large to return in one response: DemandForecastSnapshotExportDetail/fetchby/demandForecastSnapshotFilters and HierarchyLevelActualsExportDetail/fetchby/dateRangeHierarchyNodeFilters. They take the same page/pageSize parameters and return the same envelope as every other endpoint; the only difference is that omitting both returns the first page rather than the full set. Walk them with NextPage exactly as above.
NOTE: Not every endpoint supports paging. Please reference either the HTML guide or the JSON file to see more details.
Code samples
Each sample authenticates and makes one query. Replace the base URL and siq_XXXXXXXXXXXX with your instance and token.
curl
BASE=https://yourcompany.stockiqtech.net
# 1. Exchange the API token for a 1-hour bearer JWT
JWT=$(curl -s -X POST "$BASE/api/authenticate" \
-H "Content-Type: application/json" \
-d '{"token":"siq_XXXXXXXXXXXX"}' | jq -r .access_token)
# 2. First page of item-sites (empty filter = everything the token may see)
curl -s -X POST "$BASE/api/ItemSiteDetail/fetchby/hierarchyNodeFilters?page=1&pageSize=1000" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{}'Python
import requests
BASE = "https://yourcompany.stockiqtech.net"
# 1. Exchange the API token for a 1-hour bearer JWT
auth = requests.post(f"{BASE}/api/authenticate", json={"token": "siq_XXXXXXXXXXXX"})
auth.raise_for_status()
headers = {"Authorization": f"Bearer {auth.json()['access_token']}"}
# 2. Walk every page of open supply order lines for one supplier
filters = {"PrimarySupplierCodes": ["ACME"]}
rows, page_number = [], 1
while page_number is not None:
page = requests.post(
f"{BASE}/api/OpenSupplyOrderLineDetail/fetchby/dateRangeItemSiteFilters",
params={"page": page_number, "pageSize": 5000},
headers=headers, json=filters)
page.raise_for_status()
data = page.json()
rows += data["Data"]
page_number = data["NextPage"] # null after the last page
print(f"{len(rows)} rows")JavaScript (Node 18+ or browser)
const BASE = 'https://yourcompany.stockiqtech.net';
// 1. Exchange the API token for a 1-hour bearer JWT
const authRes = await fetch(`${BASE}/api/authenticate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: 'siq_XXXXXXXXXXXX' }),
});
if (!authRes.ok) throw new Error(`authenticate failed: ${authRes.status}`);
const { access_token } = await authRes.json();
// 2. Sales orders shipped in Q1 for one customer
const res = await fetch(
`${BASE}/api/SalesOrderDetail/fetchby/salesOrderDetailFilters?pageSize=1000`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
StartDate: '2026-01-01',
EndDate: '2026-03-31',
CustomerCodes: ['ACME'],
}),
});
if (!res.ok) throw new Error(`request failed: ${res.status}`);
const page = await res.json();
console.log(`${page.Data.length} of ${page.TotalRecords} rows`);PowerShell
$base = 'https://yourcompany.stockiqtech.net'
# 1. Exchange the API token for a 1-hour bearer JWT
$auth = Invoke-RestMethod -Method Post -Uri "$base/api/authenticate" `
-ContentType 'application/json' -Body '{"token":"siq_XXXXXXXXXXXX"}'
$headers = @{ Authorization = "Bearer $($auth.access_token)" }
# 2. The first page of item-sites for two sites
$filters = @{ SiteCodes = @('MAIN', 'DC-EAST') } | ConvertTo-Json
$page = Invoke-RestMethod -Method Post -Headers $headers -ContentType 'application/json' `
-Uri "$base/api/ItemSiteDetail/fetchby/hierarchyNodeFilters?page=1" `
-Body $filters
"$($page.Data.Count) of $($page.TotalRecords) rows"
$page.Data | Format-TableC#
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
var http = new HttpClient { BaseAddress = new Uri("https://yourcompany.stockiqtech.net") };
// 1. Exchange the API token for a 1-hour bearer JWT
var authResponse = await http.PostAsJsonAsync("api/authenticate", new { token = "siq_XXXXXXXXXXXX" });
authResponse.EnsureSuccessStatusCode();
var auth = await authResponse.Content.ReadFromJsonAsync<JsonElement>();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", auth.GetProperty("access_token").GetString());
// 2. A page of item-sites for one buyer
var filters = new { BuyerCodes = new[] { "JSMITH" } };
var response = await http.PostAsJsonAsync(
"api/ItemSiteDetail/fetchby/hierarchyNodeFilters?page=1&pageSize=1000", filters);
response.EnsureSuccessStatusCode();
var page = await response.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"{page.GetProperty("Data").GetArrayLength()} of {page.GetProperty("TotalRecords").GetInt32()} rows");Data security
Results are automatically scoped to the sites, suppliers, buyers, and categories your API token is granted; some fields may be omitted based on the token's permissions (for example costs and prices).
/api/authenticate, then send the returned JWT as Authorization: Bearer <token> on every request. Tokens expire after one hour. Contact support@stockiqtech.com to have an API token issued.Rate limits
API traffic is rate limited so that one busy integration cannot slow your instance down for everyone else. Limits are counted per API token, and the figures below are the defaults. If you run into rate limits, we suggest consolidating your API requests, or you can contact support to chat through your alternative options
| What is limited | Default limit | Counted per |
|---|---|---|
Requests to any /api endpoint |
100 per 60-second rolling window | API token |
POST /api/authenticate |
5 attempts per 60 seconds | Calling IP address |
Concurrent PUT /api/customexport and PUT /api/uploadorder
|
2 at a time | API token |
The general limit uses a rolling window rather than a fixed one: your allowance is replenished continuously, in roughly 20-second increments, instead of all at once at the top of each minute. A client that paces itself is therefore never shut out for a full minute.
The two long-running endpoints are capped by how many run at once, not by how often you call them. A third simultaneous export or order upload on the same token is refused immediately; it is not queued.
NOTE: The authenticate limit is counted per IP address rather than per token, because the request is anonymous until the token has been validated. Integrations that share an outbound IP address therefore share that budget of 5. This is not a constraint in practice provided you exchange the token for a JWT once and reuse the JWT for its full hour — do not call /api/authenticate ahead of each request.
What a rate-limited response looks like
A request that exceeds a limit is rejected with 429 Too Many Requests and a JSON body describing the problem:
HTTP/1.1 429 Too Many Requests
Retry-After: 20
{
"status": 429,
"title": "Too Many Requests",
"detail": "Rate limit exceeded. Please wait and retry.",
"instance": "/api/ItemSiteDetail/fetchby/hierarchyNodeFilters"
}Rejections from the two time-window limits also carry a Retry-After header giving a whole number of seconds to wait; honour it rather than retrying on a timer of your own. The concurrency limit has no wait time to report — there is no way to know when an in-flight export will finish — so those responses carry no Retry-After. Retry once one of your own in-flight requests has completed.
IMPORTANT: A 429 means the request was not processed. No data was read, written, or changed. It is always safe to wait and send exactly the same request again — including for the PUT endpoints.
Staying under the limits
-
Ask for bigger pages, not more pages. A 10,000-row page costs the same single request as a 100-row page, so a full extract at
pageSize=10000uses a hundredth of the allowance it would atpageSize=100. - Walk pages one at a time. Requesting pages in parallel is the quickest way to reach 100 requests in a minute, and it gains little — the pages of one walk are served from a snapshot, so a sequential walk is already consistent.
-
Reuse the JWT for its full hour. Re-authenticate only after a request comes back
401 Unauthorized. - Give each integration its own API token. Allowances are counted per token, so a nightly ERP sync and a Power BI refresh on separate tokens never compete for the same 100 requests a minute. Separate tokens are also easier to audit and to rotate.
- Treat a 429 as a pause, not an error. Log it, wait the advertised interval, and resume from the page you were on. Retrying instantly in a loop simply produces more 429s.
NOTE: These limits apply only to API-token traffic. Signed-in users working in the StockIQ web application are not rate limited, and their activity does not consume your integration's allowance.
Excel and Power BI (Power Query)
In Excel choose Data > Get Data > From Other Sources > Blank Query; in Power BI Desktop choose Get data > Blank query. Open Advanced Editor, paste the query below, and adjust the base URL, token, endpoint, and filter body. When prompted for credentials choose Anonymous — authentication happens inside the query itself, and because the query re-authenticates on every refresh it also works with scheduled refresh in the Power BI service.
let
BaseUrl = "https://yourcompany.stockiqtech.net",
ApiToken = "siq_XXXXXXXXXXXX",
// 1. Exchange the API token for a 1-hour bearer JWT (runs on every refresh)
Auth = Json.Document(Web.Contents(BaseUrl, [
RelativePath = "api/authenticate",
Content = Json.FromValue([token = ApiToken]),
Headers = [#"Content-Type" = "application/json"]
])),
// 2. Pull the data - edit the endpoint and the filter body as needed
Response = Json.Document(Web.Contents(BaseUrl, [
RelativePath = "api/ItemSiteDetail/fetchby/hierarchyNodeFilters",
Query = [page = "1", pageSize = "10000"],
Content = Text.ToBinary("{}"), // e.g. "{""SiteCodes"": [""MAIN""]}"
Headers = [
#"Content-Type" = "application/json",
#"Authorization" = "Bearer " & Auth[access_token]
]
])),
Result = Table.FromRecords(Response[Data])
in
ResultA single page tops out at 10,000 rows (TotalRecords in the response tells you whether more exist). For larger pulls either narrow the filter body or loop pages with List.Generate until NextPage is null.