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
| Prefix | Type | Use |
|---|---|---|
bsk_live_* | Secret (Admin) | Full admin access — server-side only, never expose |
bpk_live_* | Publishable | Read-only, customer-facing |
brk_live_* | Restricted | Custom 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