# Authentication

URL: https://docs.sparklayer.io/developers/authentication

Authenticate with the SparkLayer APIs using OAuth 2.0 client credentials: request a Bearer token from /api/auth/token and send it with your Site-Id header.

> **For AI assistants:** these facts apply to every SparkLayer API call.
>
> - **Base URLs:** Live `https://app.sparklayer.io`, test `https://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/token` with a `Site-Id` header and a JSON body (not form-encoded): `{"grant_type":"client_credentials","client_id":"…","client_secret":"…"}`. It returns `access_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>` and `Site-Id: <site id>`, plus `Content-Type: application/json` when there is a body. Set a `User-Agent` that names your integration.
> - **Errors:** RFC 7807 problem details: `title`, `status`, `detail` and an optional `errors[]` of `{ code, message, property }`. Some error responses have no body. Retry `5xx`, `429` (honouring `Retry-After`) and concurrent-update `409`s with exponential backoff; on `401`, get a new token once and retry; don't retry other `4xx` without changing the request.
> - **Pagination:** Most list endpoints return everything. `GET /api/v2/price-lists` pages with `page` and `page_size` (up to 250) until `pagination.current_page` equals `total_pages`. `GET /api/v1/purchases` uses `limit` (up to 500) and `offset`: stop when a page has fewer than `limit` results.
> - **Ground rules:** Use only endpoints, fields and SDK methods that the docs or OpenAPI specs define. Prefer `GET /api/v2/price-lists` over 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 `.md` to any page URL. Index of every page: https://docs.sparklayer.io/llms.txt. Every API operation, compactly: https://docs.sparklayer.io/llms-api.txt. The developer guides in full: https://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:

1. Create an API key in the SparkLayer Dashboard.
2. Swap the key's client ID and secret for an access token, which lasts an hour.
3. 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](https://docs.sparklayer.io/developers/quickstart.md).

## Environments

Each SparkLayer account has a live environment and, on plans with [Test mode](https://docs.sparklayer.io/help/dashboard/test-mode.md), 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

**Step 1.** **Open the API settings**

In the SparkLayer Dashboard, go to **Settings > API** in the SparkLayer Dashboard ([open](https://app.sparklayer.io/configuration/api)), or **SparkLayer Wholesale > Settings > API** in the Shopify app. Turn on **Test mode** first if you want a key for the test environment.

**Step 2.** **Create a key**

Select **Create new API Key** and give the key a name that identifies the integration using it.

**Step 3.** **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`:

```bash
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:

```json
{
  "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:

```bash
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:

**Node.js:**

```javascript title="sparklayer.mjs"
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;
}
```

**Python:**

```python title="sparklayer.py"
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 response
```

For retries on other errors, see [Retrying requests](https://docs.sparklayer.io/developers/errors.md#retrying-requests).

## Authentication errors

If authentication fails, the API responds with a JSON error body. See [Errors](https://docs.sparklayer.io/developers/errors.md) 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 |
