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.
NOTE: You can read documentation for individual endpoints by downloading the attached HTML guide at the very bottom of this article
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. This is the single most important setting — grant only the access the integration actually needs.
- 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 — a long string beginning with eyJ.
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 every query endpoint — 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.
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.