Pandabase
Learn

API Tokens

Authenticate with the Store API using Bearer tokens or HMAC signatures.

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 → Developers → 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

In HMAC mode, you sign the request body with a key derived from your secret. Send the token ID and signature in the headers; the secret stays on your server.

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)

Use the derived key to sign the request body.

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.

ResourceReadWrite
ProductsPRODUCTS_READPRODUCTS_WRITE
OrdersORDERS_READORDERS_WRITE
CustomersCUSTOMERS_READCUSTOMERS_WRITE
CategoriesCATEGORIES_READCATEGORIES_WRITE
CouponsCOUPONS_READCOUPONS_WRITE
WebhooksWEBHOOKS_READWEBHOOKS_WRITE
AnalyticsANALYTICS_READ—
PayoutsPAYOUTS_READPAYOUTS_WRITE
LicensesLICENSES_READLICENSES_WRITE
RefundsREFUNDS_READREFUNDS_WRITE
SubscriptionsSUBSCRIPTIONS_READSUBSCRIPTIONS_WRITE
UsageUSAGE_READUSAGE_WRITE
MetersMETERS_READMETERS_WRITE
ReportsREPORTS_READREPORTS_WRITE
StoreSTORE_READSTORE_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.

Last updated on

On this page