Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

API Key Setup

A guide to creating, scoping, and managing API keys. Covers restricted keys for backend services, customer-scoped keys for metering, and key revocation.

Key Prefixes

PrefixTypeUse
bsk_live_*Secret (Admin)Full admin access — server-side only, never expose
bpk_live_*PublishableRead-only, customer-facing
brk_live_*RestrictedCustom scope — for specific backend services

Create a Restricted Key (events:write + metering:write)

Restricted keys let you give a backend service exactly the permissions it needs — no more. A metering service only needs events:write:

# [curl]
curl -X POST https://api.bill.sh/admin/v1/api-keys \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Metering Service — Production",
    "scopes": ["events:write", "metering:read"],
    "description": "Used by the metering sidecar to ingest usage events"
  }'
# [Python]
import requests

resp = requests.post(
    "https://api.bill.sh/admin/v1/api-keys",
    headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
    json={
        "name": "Metering Service — Production",
        "scopes": ["events:write", "metering:read"],
        "description": "Used by the metering sidecar to ingest usage events",
    },
)
key_data = resp.json()
restricted_key = key_data["key"]   # brk_live_xxxxx — store this securely!
key_id = key_data["id"]
print(f"Restricted key: {restricted_key[:16]}... (ID: {key_id})")
# WARNING: the key value is only returned once — store it in your secrets manager
// [Node.js]
const resp = await fetch("https://api.bill.sh/admin/v1/api-keys", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${ADMIN_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Metering Service — Production",
    scopes: ["events:write", "metering:read"],
    description: "Used by the metering sidecar to ingest usage events",
  }),
});
const keyData = await resp.json();
const restrictedKey = keyData.key;  // brk_live_xxxxx — only returned once!
const keyId = keyData.id;
console.log(`Restricted key created: ${restrictedKey.slice(0, 16)}... (ID: ${keyId})`);
// [Go]
body, _ := json.Marshal(map[string]interface{}{
    "name":        "Metering Service — Production",
    "scopes":      []string{"events:write", "metering:read"},
    "description": "Used by the metering sidecar to ingest usage events",
})
req, _ := http.NewRequest("POST", "https://api.bill.sh/admin/v1/api-keys",
    bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+adminToken)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()

var keyData map[string]interface{}
json.NewDecoder(resp.Body).Decode(&keyData)
fmt.Printf("Key: %v (ID: %v)\n", keyData["key"], keyData["id"])
// Store keyData["key"] in your secrets manager immediately

Validate a Key in Your Integration

Before trusting a key, verify it’s valid and has the expected scopes:

# [curl]
curl https://api.bill.sh/v1/api-keys/validate \
  -H "Authorization: Bearer $RESTRICTED_KEY"
# [Python]
resp = requests.get(
    "https://api.bill.sh/v1/api-keys/validate",
    headers={"Authorization": f"Bearer {RESTRICTED_KEY}"},
)
validation = resp.json()
print(f"Key valid: {validation['valid']}")
print(f"Scopes: {validation['scopes']}")
print(f"Key ID: {validation['key_id']}")

# Check the key has the scope you need
if "events:write" not in validation["scopes"]:
    raise PermissionError("Key missing events:write scope")
// [Node.js]
const resp = await fetch("https://api.bill.sh/v1/api-keys/validate", {
  headers: { "Authorization": `Bearer ${RESTRICTED_KEY}` },
});
const validation = await resp.json();
console.log("Valid:", validation.valid);
console.log("Scopes:", validation.scopes.join(", "));

if (!validation.scopes.includes("events:write")) {
  throw new Error("Key missing events:write scope");
}
// [Go]
req, _ := http.NewRequest("GET", "https://api.bill.sh/v1/api-keys/validate", nil)
req.Header.Set("Authorization", "Bearer "+restrictedKey)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()

var validation map[string]interface{}
json.NewDecoder(resp.Body).Decode(&validation)
fmt.Printf("Valid: %v, Scopes: %v\n", validation["valid"], validation["scopes"])

Create a Customer-Scoped Key (Option D)

For multi-tenant architectures, create a key scoped to a specific customer. This key can only read/write data for that customer — perfect for giving a customer programmatic access to their own billing data:

# [curl]
curl -X POST https://api.bill.sh/admin/v1/customers/$CUSTOMER_ID/api-keys \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Corp — Integration Key",
    "scopes": ["events:write", "subscriptions:read", "invoices:read"],
    "description": "Acme self-service integration — created by their ops team"
  }'
# [Python]
resp = requests.post(
    f"https://api.bill.sh/admin/v1/customers/{customer_id}/api-keys",
    headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
    json={
        "name": "Acme Corp — Integration Key",
        "scopes": ["events:write", "subscriptions:read", "invoices:read"],
        "description": "Acme self-service integration — created by their ops team",
    },
)
key_data = resp.json()
print(f"Customer-scoped key: {key_data['key'][:20]}...")
print(f"Customer scope enforced for: {customer_id}")
# This key can ONLY access data for customer_id — enforced server-side
// [Node.js]
const resp = await fetch(
  `https://api.bill.sh/admin/v1/customers/${customerId}/api-keys`,
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${ADMIN_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      name: "Acme Corp — Integration Key",
      scopes: ["events:write", "subscriptions:read", "invoices:read"],
      description: "Acme self-service integration",
    }),
  }
);
const keyData = await resp.json();
console.log(`Customer key: ${keyData.key.slice(0, 20)}...`);
// This key is scoped to customerId — any attempt to access other customers returns 403
// [Go]
body, _ := json.Marshal(map[string]interface{}{
    "name":        "Acme Corp — Integration Key",
    "scopes":      []string{"events:write", "subscriptions:read", "invoices:read"},
    "description": "Acme self-service integration",
})
req, _ := http.NewRequest("POST",
    "https://api.bill.sh/admin/v1/customers/"+customerID+"/api-keys",
    bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+adminToken)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var keyData map[string]interface{}
json.NewDecoder(resp.Body).Decode(&keyData)
fmt.Printf("Customer key: %.20s...\n", keyData["key"])

Revoke a Key

Immediately invalidate a key — useful when rotating credentials or offboarding a service:

# [curl]
curl -X DELETE https://api.bill.sh/admin/v1/api-keys/$KEY_ID \
  -H "Authorization: Bearer $ADMIN_TOKEN"
# [Python]
resp = requests.delete(
    f"https://api.bill.sh/admin/v1/api-keys/{key_id}",
    headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
)
print(f"Revoked: {resp.status_code == 200}")
// [Node.js]
const resp = await fetch(`https://api.bill.sh/admin/v1/api-keys/${keyId}`, {
  method: "DELETE",
  headers: { "Authorization": `Bearer ${ADMIN_TOKEN}` },
});
console.log("Revoked:", resp.ok);
// [Go]
req, _ := http.NewRequest("DELETE",
    "https://api.bill.sh/admin/v1/api-keys/"+keyID, nil)
req.Header.Set("Authorization", "Bearer "+adminToken)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
fmt.Println("Revoked:", resp.StatusCode == 200)

Security Best Practices

  • Never expose bsk_live_* keys in client-side code, browser environments, or version control
  • Use restricted keys (brk_live_*) for any service that only needs specific scopes
  • Rotate keys regularly — revoke old keys after creating new ones
  • Store in secrets managers — AWS Secrets Manager, GCP Secret Manager, Vault, etc.
  • The key value is only returned once at creation time — store it immediately or you’ll need to revoke and regenerate