Getting Started
Everything you need to authenticate, make your first API call, and start building with the SpireStock REST API.
Prerequisites
- An active SpireStock workspace with admin or developer access.
- An API key, created from the dashboard under Developer β API Keys. Creating one requires workspace administrator access.
- A tool for making HTTP requests β cURL, Postman, or any HTTP client library.
Don't have a workspace yet?
Sign up at app.spirestock.com/signup to create a free workspace and start building immediately.
Base URL
All API endpoints are served from a single base URL. Every path referenced in this documentation is relative to:
https://api.spirestock.com/api/v1For example, the full URL for listing orders would be https://api.spirestock.com/api/v1/orders.
Authentication
SpireStock accepts two credentials. Which one you want depends on whether a browser is involved.
| Credential | Use it for | Lifetime |
|---|---|---|
API key β X-API-Key | Servers, scheduled jobs, ERP connectors, anything without a browser | Until you revoke it (or its optional expiry) |
JWT β Authorization: Bearer | Browser sessions signed in through the dashboard | 15 days, and only one active per user |
Server-side? Use an API key.
/auth/login requires a Cloudflare Turnstile token, which only a browser can obtain β a server calling it gets 400 Security verification required. On top of that, a user holds only one valid JWT at a time, so signing into the dashboard invalidates the token your integration was using. API keys have neither problem.
Authenticating with an API key
Create a key in the dashboard under Developer β API Keys, or with the API itself while signed in as an administrator:
curl -X POST https://api.spirestock.com/api/v1/developer/api-keys \
-H "Authorization: Bearer YOUR_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Warehouse sync",
"scopes": "read,orders",
"expires_in_days": 90
}'{
"response_code": 201,
"message": "API key created. Save this key β it won't be shown again.",
"data": {
"id": 3,
"key": "df_9f3a1c7e2b8d4a6f0c5e1b7a3d9f2e4c6a8b0d2f4e6a8c0b2d4f6a8c0e2b4d6f",
"name": "Warehouse sync",
"key_prefix": "df_9f3a1c",
"scopes": "read,orders",
"expires_at": "2026-11-16T00:00:00.000Z"
}
}The key is shown exactly once
Only a SHA-256 hash of the key is stored, so it cannot be retrieved later β not by you, not by support. Save it to your secret store the moment you create it. Lost a key? Revoke it and issue a new one.
Send it on every request as a header:
X-API-Key: df_9f3a1c7e2b8d...Scopes
A keyβs scopes string mixes two kinds of token: actions (read, write) and optional resources (orders, users, products, β¦). read permits GET and HEAD; write permits every method and implies read. Name no resource and the key reaches every endpoint it is allowed to; name one or more and it is confined to those.
| Scopes | What the key can do |
|---|---|
read | Read any endpoint |
read,orders | Read /orders and nothing else |
write,orders | Full access to /orders, read and write |
read,write | Full access to every allowed endpoint |
* | The same as read,write |
Endpoints an API key cannot reach
Regardless of scope, keys are refused on /auth, /signup, /developer, /platform, /super-admin, /portal, /driver-app and /support. That is why key and webhook management needs a signed-in administrator β a leaked key cannot mint more keys or repoint your webhooks.
Authenticating with a session token
If you are building something a person signs into, exchange their credentials for a JWT. The request must carry a turnstile_token from the Turnstile widget on your page:
curl -X POST https://api.spirestock.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-H "User-Agent: AcmeIntegration/1.0" \
-d '{
"email": "YOUR_EMAIL",
"password": "YOUR_PASSWORD",
"turnstile_token": "TOKEN_FROM_THE_TURNSTILE_WIDGET"
}'{
"response_code": 200,
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 1,
"first_name": "Jane",
"last_name": "Developer",
"official_email": "[email protected]",
"role_name": "Admin",
"user_type": 0,
"organization_id": 5,
"organization_name": "Acme Dairy"
},
"workspace": {
"workspace_id": "ws_x9y8z7",
"slug": "acme-dairy",
"display_name": "Acme Dairy",
"plan": "professional",
"status": 1
}
}Include the token in the Authorization header using the Bearer scheme:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Token expiry & single active session
Tokens expire after 15 days. Only one is valid per user at a time β logging in again, logging out, or changing the password invalidates the previous one. When you receive 401 Unauthorized, re-authenticate.
Token storage best practices
Never store JWT tokens in localStorage or sessionStorage β they are accessible to any JavaScript running on the page (XSS-vulnerable). Instead:
- Server-side apps: Store tokens in memory or encrypted server-side sessions.
- Browser apps: Use
httpOnlycookies set by your backend proxy, or keep tokens in memory only (they will be lost on page refresh, which is the safer trade-off). - Never include tokens in URLs, log files, or error reporting payloads.
Set a User-Agent
The API rejects requests whose User-Agent is missing, shorter than 10 characters, or matches a known scraping tool. Several HTTP clients trip this by default:
| Client | Sends by default | Result |
|---|---|---|
| cURL | curl/8.5.0 | 403 |
Python requests | python-requests/2.31.0 | 403 |
Node fetch | node (4 characters) | 403 |
Two ways to satisfy this
Send X-API-Key β keyed requests skip the check entirely, because the key already identifies you. Or set a descriptive User-Agent naming your integration, which is good practice either way:
curl https://api.spirestock.com/api/v1/orders \
-H "X-API-Key: YOUR_API_KEY" \
-H "User-Agent: AcmeIntegration/1.0"const res = await fetch("https://api.spirestock.com/api/v1/orders", {
headers: {
"X-API-Key": process.env.SPIRESTOCK_API_KEY,
"User-Agent": "AcmeIntegration/1.0",
},
});import requests
session = requests.Session()
session.headers.update({
"X-API-Key": os.environ["SPIRESTOCK_API_KEY"],
"User-Agent": "AcmeIntegration/1.0",
})Your First Request
With a key in hand, fetch a list of orders from your workspace. Keep the key in an environment variable β never in source control.
curl https://api.spirestock.com/api/v1/orders \
-H "X-API-Key: $SPIRESTOCK_API_KEY"const res = await fetch("https://api.spirestock.com/api/v1/orders", {
headers: {
"X-API-Key": process.env.SPIRESTOCK_API_KEY,
},
});
const { data, pagination } = await res.json();
console.log(data); // Array of order objects
console.log(pagination.total);import os
import requests
res = requests.get(
"https://api.spirestock.com/api/v1/orders",
headers={"X-API-Key": os.environ["SPIRESTOCK_API_KEY"]},
)
result = res.json()
print(result["data"]) # List of order objects
print(result["pagination"]["total"])Every request is scoped to the organization the key belongs to. There is no tenant parameter to pass and no way to reach another workspaceβs data.
Rate Limits
To ensure fair usage across all tenants, the API enforces rate limits on a per-IP or per-token basis. When a limit is exceeded the API responds with 429 Too Many Requests.
| Endpoint Group | Limit | Window |
|---|---|---|
POST /signup/register | 5 requests | Per hour |
POST /auth/login | 20 requests | Per 15 minutes |
| General endpoints | 120 requests | Per minute |
| General endpoints | 2,000 requests | Per hour |
| Export endpoints | 5 requests | Per minute |
| Report endpoints | 20 requests | Per minute |
Rate-limit headers
Every response includes X-RateLimit-Limit and X-RateLimit-Remaining so you can throttle before you are throttled. A 429 also carries Retry-After in seconds.
Each key gets its own budget
An API key is metered separately from the administrator who created it, and separately from every other key. A busy integration cannot rate-limit a colleague out of the dashboard, and splitting work across two keys gives you two budgets.
Error Handling
The API uses standard HTTP status codes. Errors include a JSON body with a response_code matching the HTTP status and a human-readable message field.
{
"response_code": 400,
"message": "Validation failed"
}| Status Code | Meaning | Common Cause |
|---|---|---|
400 | Bad Request | Invalid or missing request body fields |
401 | Unauthorized | Missing, revoked, or expired credential |
403 | Forbidden | Missing scope, an endpoint API keys cannot reach, a suspended workspace, or a rejected User-Agent |
404 | Not Found | Resource does not exist or belongs to another workspace |
429 | Too Many Requests | Rate limit exceeded β back off and retry |
500 | Internal Server Error | Unexpected server-side failure β contact support if it persists |
Next Steps
Now that you can authenticate and make requests, explore further:
- API Reference β Full endpoint documentation with request/response schemas.
- Webhooks β Receive real-time event notifications for orders, users, and payments.
- SDKs β Official client libraries for Node.js, Python, and cURL examples.