Skip to main content
We will assume you have a general understanding of programming. This guide is intended for developers, and we expect you to have a good understanding of REST APIs in general. If you’re not a developer, please skip this section.

Overview

API tokens authenticate requests to the Store API at /v2/core/stores/:storeId/*. Each token has a set of permissions and uses one of two authentication modes: Bearer or HMAC. Create tokens in your store’s dashboard under Settings → API Tokens, or via the tokens API.

Bearer mode

Send the token secret directly in the Authorization header. The secret starts with sk_.
Authorization: Bearer sk_live_abc123...

Example (TypeScript)

const STORE_ID = process.env.PANDABASE_STORE_ID!;
const API_TOKEN = process.env.PANDABASE_API_TOKEN!; // sk_live_...

// list products
const res = await fetch(
  `https://api.pandabase.io/v2/core/stores/${STORE_ID}/products`,
  {
    headers: {
      Authorization: `Bearer ${API_TOKEN}`,
    },
  },
);

const { data } = await res.json();
console.log(data.items);

Example (cURL)

curl https://api.pandabase.io/v2/core/stores/shp_xxx/products \
  -H "Authorization: Bearer sk_live_abc123..."
Bearer mode is simple and works well for server-side integrations where the token is stored securely.

HMAC mode

HMAC mode never sends the secret over the wire. Instead, you sign each request body with your token’s secret using HMAC-SHA256, and send the token ID + signature in headers. When you create an HMAC token, you receive:
  • Token ID — starts with stk_, sent in the Authorization header
  • Secret — starts with sk_, used to derive the signing key (never sent in requests)

How signing works

The HMAC key is not your raw secret. You must first hash your secret with SHA-256 to derive the signing key, then use that key to compute the HMAC-SHA256 signature of the request body.
signing_key = SHA-256(your_secret)
signature   = HMAC-SHA256(signing_key, request_body)
This ensures your raw secret is never used directly in cryptographic operations.

Headers

HeaderValue
AuthorizationBearer stk_your_token_id
X-SignatureHMAC-SHA256 hex digest of the request body, keyed with the derived signing key
For requests with no body (GET, DELETE), sign an empty string.

Example (TypeScript)

import crypto from "crypto";

const STORE_ID = process.env.PANDABASE_STORE_ID!;
const TOKEN_ID = process.env.PANDABASE_TOKEN_ID!;   // stk_...
const TOKEN_SECRET = process.env.PANDABASE_TOKEN_SECRET!; // sk_...

// derive the signing key by hashing the secret
const SIGNING_KEY = crypto
  .createHash("sha256")
  .update(TOKEN_SECRET)
  .digest("hex");

function sign(body: string): string {
  return crypto
    .createHmac("sha256", SIGNING_KEY)
    .update(body)
    .digest("hex");
}

// GET request (sign empty string)
async function listProducts() {
  const signature = sign("");

  const res = await fetch(
    `https://api.pandabase.io/v2/core/stores/${STORE_ID}/products`,
    {
      headers: {
        Authorization: `Bearer ${TOKEN_ID}`,
        "X-Signature": signature,
      },
    },
  );

  return res.json();
}

// POST request (sign the JSON body)
async function createProduct() {
  const body = JSON.stringify({
    name: "New Product",
    amount: 2999,
    type: "ONE_TIME",
  });

  const signature = sign(body);

  const res = await fetch(
    `https://api.pandabase.io/v2/core/stores/${STORE_ID}/products`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${TOKEN_ID}`,
        "X-Signature": signature,
      },
      body,
    },
  );

  return res.json();
}

Example (Python)

import hmac
import hashlib
import json
import requests
import os

STORE_ID = os.environ["PANDABASE_STORE_ID"]
TOKEN_ID = os.environ["PANDABASE_TOKEN_ID"]        # stk_...
TOKEN_SECRET = os.environ["PANDABASE_TOKEN_SECRET"] # sk_...

# derive the signing key by hashing the secret
SIGNING_KEY = hashlib.sha256(TOKEN_SECRET.encode()).hexdigest()

def sign(body: str) -> str:
    return hmac.new(
        SIGNING_KEY.encode(),
        body.encode(),
        hashlib.sha256
    ).hexdigest()

# GET request
def list_products():
    signature = sign("")
    res = requests.get(
        f"https://api.pandabase.io/v2/core/stores/{STORE_ID}/products",
        headers={
            "Authorization": f"Bearer {TOKEN_ID}",
            "X-Signature": signature,
        },
    )
    return res.json()

# POST request
def create_product():
    body = json.dumps({"name": "New Product", "amount": 2999, "type": "ONE_TIME"})
    signature = sign(body)
    res = requests.post(
        f"https://api.pandabase.io/v2/core/stores/{STORE_ID}/products",
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {TOKEN_ID}",
            "X-Signature": signature,
        },
        data=body,
    )
    return res.json()

Example (cURL)

# derive signing key: SHA-256 of your secret
SIGNING_KEY=$(echo -n "$TOKEN_SECRET" | openssl dgst -sha256 | awk '{print $2}')

# GET — sign empty string
SIGNATURE=$(echo -n "" | openssl dgst -sha256 -hmac "$SIGNING_KEY" | awk '{print $2}')

curl https://api.pandabase.io/v2/core/stores/shp_xxx/products \
  -H "Authorization: Bearer stk_xxx" \
  -H "X-Signature: $SIGNATURE"
# POST — sign the JSON body
BODY='{"name":"New Product","amount":2999,"type":"ONE_TIME"}'
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_KEY" | awk '{print $2}')

curl -X POST https://api.pandabase.io/v2/core/stores/shp_xxx/products \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer stk_xxx" \
  -H "X-Signature: $SIGNATURE" \
  -d "$BODY"

Bearer vs HMAC

BearerHMAC
How it worksSecret sent in every requestSecret never leaves your server — only a derived signature is sent
SetupSimple — one headerMore complex — derive signing key + compute signature per request
SecurityRelies on TLSVerifies request integrity + TLS
Best forServer-side integrations, scriptsHigh-security environments, multi-party systems
Risk if interceptedToken can be reusedSignature is only valid for that specific request body
Both modes use constant-time comparison to prevent timing attacks. HMAC signatures are always 64-character hex strings (SHA-256 output).

Permissions

Tokens use granular permissions. Only grant what your integration needs.
PRODUCTS_READ    PRODUCTS_WRITE
ORDERS_READ      ORDERS_WRITE
CUSTOMERS_READ   CUSTOMERS_WRITE
CATEGORIES_READ  CATEGORIES_WRITE
COUPONS_READ     COUPONS_WRITE
WEBHOOKS_READ    WEBHOOKS_WRITE
ANALYTICS_READ
PAYOUTS_READ     PAYOUTS_WRITE
LICENSES_READ    LICENSES_WRITE
REFUNDS_READ     REFUNDS_WRITE
STORE_READ       STORE_WRITE
If a request requires a permission the token doesn’t have, the API returns 403 Forbidden with the missing permissions listed in the error message.

Secret rotation

Rotate a token’s secret at any time via the dashboard or POST /v2/stores/:storeId/tokens/:tokenId/roll-secret. The old secret is immediately invalidated — update your integration before rotating.

Request logging

All Store API requests are automatically logged with 30-day retention. Each log entry captures the method, path, status code, response time, and IP address. View logs in the dashboard or via GET /v2/stores/:storeId/api-logs.
Treat API tokens like passwords. Never expose them in client-side code, public repositories, or logs. If a token is compromised, rotate or delete it immediately.