Authentication
For AI assistants: the facts every SparkLayer API call needs
- Base URLs: Live
https://app.sparklayer.io, testhttps://test.app.sparklayer.io. Each environment has its own data and its own API keys. - Access: API access needs the Pro or Enterprise plan. Create credentials (Site ID, Client ID, Client Secret) in the SparkLayer Dashboard at Settings > API.
- Access token:
POST {base}/api/auth/tokenwith aSite-Idheader and a JSON body (not form-encoded):{"grant_type":"client_credentials","client_id":"…","client_secret":"…"}. It returnsaccess_token, valid for 3,600 seconds. There is no refresh token: cache the token and request a new one shortly before it expires. - Every request:
Authorization: Bearer <access_token>andSite-Id: <site id>, plusContent-Type: application/jsonwhen there is a body. Set aUser-Agentthat names your integration. - Errors: RFC 7807 problem details:
title,status,detailand an optionalerrors[]of{ code, message, property }. Some error responses have no body. Retry5xx,429(honouringRetry-After) and concurrent-update409s with exponential backoff; on401, get a new token once and retry; don't retry other4xxwithout changing the request. - Pagination: Most list endpoints return everything.
GET /api/v2/price-listspages withpageandpage_size(up to 250) untilpagination.current_pageequalstotal_pages.GET /api/v1/purchasesuseslimit(up to 500) andoffset: stop when a page has fewer thanlimitresults. - Ground rules: Use only endpoints, fields and SDK methods that the docs or OpenAPI specs define. Prefer
GET /api/v2/price-listsover the deprecated v1. Products are matched by SKU. Keep credentials in environment variables or a secrets manager, never in code. - Read the docs as Markdown: Add
.mdto any page URL. Index of every page: docs.sparklayer.io/llms.txt. Every API operation, compactly: docs.sparklayer.io/llms-api.txt. The developer guides in full: docs.sparklayer.io/developers/llms.txt. - OpenAPI specs:
core,ordering,pricing,purchasing,stock,files,sync-log: https://docs.sparklayer.io/openapi/<api>.yaml (also.json). Ignite: https://docs.sparklayer.io/openapi/ignite.yaml. - MCP: Search and read these docs from your assistant with the docs MCP server at https://docs.sparklayer.io/mcp.
Every request to the SparkLayer APIs needs an access token. Getting one takes three steps:
- Create an API key in the SparkLayer Dashboard.
- Swap the key's client ID and secret for an access token, which lasts an hour.
- Send the token with every request.
This is the standard OAuth 2.0 client credentials flow.
For a step-by-step first call, see the quickstart.
Environments
Each SparkLayer account has a live environment and, on plans with Test mode, a test environment. The host you call selects the environment:
| Environment | Base URL |
|---|---|
| Live | https://app.sparklayer.io |
| Test | https://test.app.sparklayer.io |
The environments hold separate data and separate API keys. A key created while the Dashboard is in Test mode only works against the test base URL, and a key created in live mode only works against the live base URL.
Create an API key
Open the API settings
In the SparkLayer Dashboard, go to Settings,API (opens in your SparkLayer Dashboard in a new tab). Turn on Test mode first if you want a key for the test environment.
Create a key
Select Create new API Key and give the key a name that identifies the integration using it.
Copy the credentials
Copy the Site ID, Client ID and Client Secret. The client secret is only shown once, so store it securely — for example in your integration's secrets manager.
Deleting a key in the Dashboard also revokes every access token issued for it.
Request an access token
Send a POST request to /api/auth/token with your Site ID in the Site-Id header and your credentials in a JSON body. The token endpoint only accepts Content-Type: application/json:
curl -X POST https://app.sparklayer.io/api/auth/token \
-H "Site-Id: <SITE_ID>" \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "<CLIENT_ID>",
"client_secret": "<CLIENT_SECRET>"
}'| Field | Value |
|---|---|
grant_type | Always client_credentials |
client_id | The Client ID shown when you created the key |
client_secret | The Client Secret shown when you created the key |
If the credentials are valid, you receive an access token:
{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "<ACCESS_TOKEN>"
}| Field | Description |
|---|---|
token_type | Always Bearer |
expires_in | Seconds until the token expires: tokens are valid for 1 hour |
access_token | The token to send in the Authorization header |
A token only works with the Site ID and environment it was issued for. No refresh token is issued: when a token expires, request a new one with the same credentials.
Standard OAuth libraries won't work as they are
The token request is JSON with a Site-Id header, not the form-encoded body most OAuth 2.0 client libraries send. Request the token with a plain HTTP call, as below, or configure your library to send JSON and the extra header.
Call the API
Send the token in the Authorization header, together with the Site-Id header, on every request:
curl "https://app.sparklayer.io/api/v2/price-lists" \
-H "Site-Id: <SITE_ID>" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "User-Agent: acme-erp-sync/1.2"| Header | Value |
|---|---|
Site-Id | Your Site ID |
Authorization | Bearer <ACCESS_TOKEN> |
Content-Type | application/json for requests with a body |
Set a User-Agent
Set a User-Agent header that identifies your integration, such as acme-erp-sync/1.2. It helps our support team find your requests if you need help debugging an issue.
Reuse the access token
Request a token once and reuse it for every call until it's close to expiry, rather than requesting one per call. These small helpers do that, and fetch a new token when one is rejected:
const { SPARKLAYER_BASE_URL, SPARKLAYER_SITE_ID, SPARKLAYER_CLIENT_ID, SPARKLAYER_CLIENT_SECRET } =
process.env;
let token = null; // { value, expiresAt }
async function getToken() {
// reuse the token until a minute before it expires
if (token && Date.now() < token.expiresAt - 60_000) return token.value;
const response = await fetch(`${SPARKLAYER_BASE_URL}/api/auth/token`, {
method: 'POST',
headers: { 'Site-Id': SPARKLAYER_SITE_ID, 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: SPARKLAYER_CLIENT_ID,
client_secret: SPARKLAYER_CLIENT_SECRET,
}),
});
if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
const body = await response.json();
token = { value: body.access_token, expiresAt: Date.now() + body.expires_in * 1000 };
return token.value;
}
/** Calls a SparkLayer API path, e.g. sparklayer('/api/v2/price-lists'). */
export async function sparklayer(path, init = {}, retried = false) {
const response = await fetch(`${SPARKLAYER_BASE_URL}${path}`, {
...init,
headers: {
'Site-Id': SPARKLAYER_SITE_ID,
Authorization: `Bearer ${await getToken()}`,
'User-Agent': 'acme-erp-sync/1.2',
...(init.body ? { 'Content-Type': 'application/json' } : {}),
...init.headers,
},
});
if (response.status === 401 && !retried) {
token = null; // the token was rejected: get a new one and try once more
return sparklayer(path, init, true);
}
return response;
}import os
import time
import requests
BASE_URL = os.environ["SPARKLAYER_BASE_URL"]
SITE_ID = os.environ["SPARKLAYER_SITE_ID"]
_token = {"value": None, "expires_at": 0.0}
def get_token() -> str:
# reuse the token until a minute before it expires
if _token["value"] and time.time() < _token["expires_at"] - 60:
return _token["value"]
response = requests.post(
f"{BASE_URL}/api/auth/token",
headers={"Site-Id": SITE_ID},
json={
"grant_type": "client_credentials",
"client_id": os.environ["SPARKLAYER_CLIENT_ID"],
"client_secret": os.environ["SPARKLAYER_CLIENT_SECRET"],
},
timeout=30,
)
response.raise_for_status()
body = response.json()
_token.update(value=body["access_token"], expires_at=time.time() + body["expires_in"])
return _token["value"]
def sparklayer(method: str, path: str, retried: bool = False, **kwargs) -> requests.Response:
"""Calls a SparkLayer API path, e.g. sparklayer("GET", "/api/v2/price-lists")."""
headers = {
"Site-Id": SITE_ID,
"Authorization": f"Bearer {get_token()}",
"User-Agent": "acme-erp-sync/1.2",
**kwargs.pop("headers", {}),
}
response = requests.request(method, f"{BASE_URL}{path}", headers=headers, timeout=60, **kwargs)
if response.status_code == 401 and not retried:
_token["value"] = None # the token was rejected: get a new one and try once more
return sparklayer(method, path, retried=True, **kwargs)
return responseFor retries on other errors, see Retrying requests.
Authentication errors
If authentication fails, the API responds with a JSON error body. See Errors for the format and every status code.
| Status | When |
|---|---|
400 | The token request is missing the Site-Id header, a required field or Content-Type: application/json |
401 | The client ID or secret is wrong, or the access token is missing, invalid or expired |
401 or 403 | The access token was issued for a different Site ID or environment. The Pricing and Purchasing APIs also return 403 when the Authorization header is missing |
Last updated