Developers
Authentication and API keys
How to create, scope, store, rotate, and revoke API keys for the Performance Blocks API, plus auth error handling.
Plan availability: The Performance Blocks API is available on the Agentic plan. Team plan customers can upgrade in Settings → Billing.
The Performance Blocks API uses bearer-token authentication. Every request must include an Authorization: Bearer pb_live_... header. Tokens are organization-scoped API keys that you create and manage in the admin console.
This article covers how to create keys, scope them to least privilege, store them safely, separate test from live, rotate or revoke when needed, and interpret authentication errors.
How authentication works
Every request to https://api.performanceblocks.com/v1/... carries the API key in the Authorization header:
Authorization: Bearer pb_live_4f2a9b1c8d3e7f64a1b2c3d4e5f60718
The API:
- Validates the key signature and looks up the owning organization.
- Verifies the key is active (not revoked, not expired).
- Checks that the requested operation is allowed by the key's scopes.
- Records the request against the key's rate-limit window.
If any check fails, the request is rejected before it reaches the resource handler. Successful requests are processed in the context of the key's organization — you cannot reach data outside that organization, even with elevated scopes.
There is no OAuth flow. The public API does not support delegated end-user access tokens. If you need to act on behalf of a specific user inside the application, use the application UI; the API operates at the organization level with audit attribution to the API key, not to a human user.
Creating an API key
API keys are managed by org admins.
- In the application, go to Settings → API.
- Click Create API key.
- Name the key (e.g.
Production HRIS sync,BI dashboard read-only,Local development). - Choose an environment — Test or Live.
- Choose scopes (see below).
- Optionally set an expiration date. Keys without an expiration last until you revoke them.
- Click Create. The full key is displayed once — copy it into your secret store immediately. You can never view it again; if you lose it, revoke and re-issue.
After creation, the API key list shows the key's name, prefix, scopes, environment, last-used timestamp, and creator. The full secret is no longer recoverable.
Key prefix convention
Every key has a prefix that tells you what kind of key it is:
| Prefix | Environment | Notes |
|---|---|---|
pb_live_ |
Live | Reads and writes against your production organization data. |
pb_test_ |
Test | Reads and writes against your sandbox organization data. |
Live and test data are fully separated. A pb_test_ key cannot read or modify live data and vice versa. The prefix is also visible in audit logs, making it easy to confirm that a request originated from the right environment.
Always check the prefix in your secret manager and CI variables before deploying. Mixing environments is the most common cause of "the API call worked locally but did nothing in production" bugs.
Key scopes
Scopes follow a resource:action format. The action is one of read or write. write implies read for the same resource.
| Scope | Allows |
|---|---|
employees:read |
List, get employees. |
employees:write |
Create, update, deactivate, reactivate, rehire employees; CSV upsert. |
observations:read |
List, get observations. |
observations:write |
Create, update, archive, restore observations. |
summaries:read |
List, get summaries. |
summaries:write |
Create, update, share, approve summaries; submit feedback on a summary. |
team_summaries:read |
List, get, preview team summaries. |
team_summaries:write |
Create, reassign, restore team summaries. |
conversations:read |
List, get conversations and messages. |
conversations:write |
Create conversations and messages; update / delete messages. |
objectives:read |
Read employee and manager objectives. |
objectives:write |
Create, update, transition objectives. |
assignments:read |
List, get feedback assignments and submissions. |
assignments:write |
Create, update, complete assignments. |
review_items:read |
List pending review items. |
review_items:write |
Approve, reject, request clarification. |
notifications:read |
List notifications, count unread. |
notifications:write |
Mark notifications as read. |
preferences:read |
Read user preferences. |
preferences:write |
Update user preferences. |
search:read |
Use the search endpoint. |
webhooks:read |
List webhook subscriptions and delivery logs. |
webhooks:write |
Create, update, delete webhook subscriptions; trigger test deliveries. |
session:read |
Call /session. Included by default in every key. |
There is no *:* super-scope. To grant a key full access, explicitly list the scopes it needs. This makes audit log review and incident scoping faster.
Convenience presets
The Settings → API key creation form offers three presets you can use as a starting point and then customize:
- Read-only — every
:readscope, no:write. Good for BI tools and dashboards. - HRIS integration —
employees:read,employees:write,webhooks:write. Good for one-way employee sync. - Full access — every documented scope. Use only when you genuinely need it; prefer narrower keys.
Storing keys securely
API keys are credentials. Treat them like passwords.
- Never commit keys to source control. Add
.env*files to.gitignoreand use a secret scanner in CI. - Use a secret manager for production — Cloudflare Workers Secrets, AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, or Doppler.
- Pass keys through environment variables, not config files checked into the repo.
- Use separate keys per environment. Don't share a
pb_live_key between staging and production deployments — when one rotates, the other breaks. - Use separate keys per integration. If your BI dashboard and your HRIS sync share a key and you need to revoke for one of them, you take the other down too.
A reasonable naming convention is <service>-<environment>-<purpose>, for example bi-dashboard-prod-read or workday-prod-employee-sync.
Rotating keys
Plan to rotate keys at least once a year, and immediately if you suspect compromise (key leaked into a log, ex-employee with access, etc.).
The recommended zero-downtime rotation:
- Create a new key with the same scopes as the old key.
- Deploy the new key to your secret manager and roll your services with the new value.
- Verify traffic is flowing on the new key — the API key list shows a
last_usedtimestamp. - Wait at least a full request cycle (e.g. one webhook delivery, one cron run).
- Revoke the old key from Settings → API.
If you rotate by deleting the old key first, in-flight requests will fail with 401.
Revoking keys
To revoke a key:
- Go to Settings → API.
- Find the key in the list.
- Click the menu and choose Revoke.
- Confirm.
Revocation is immediate and irreversible. Any request using the revoked key returns 401 authentication_required within seconds. Revoke a key the moment you no longer need it, no matter how recently it was created.
Audit log
Every API key action is recorded in the organization audit log:
- Key created (who, when, scopes, environment).
- Key revoked (who, when).
- Key used to perform a state-changing operation (which endpoint, which resource, which key).
Filter the audit log by API key to see every write a given key has made. This is useful when investigating an unexpected change or scoping the blast radius of a leaked key.
Read operations are not written to the audit log to keep volume manageable. Use rate-limit headers and webhook delivery logs as proxies for read traffic.
Multiple environments
Most teams keep at least three keys per integration:
- Local development — a
pb_test_key with broad scopes, scoped to a sandbox org. - Staging / preview — a
pb_test_key with the same scopes you'll use in production. - Production — a
pb_live_key with the narrowest scopes that work.
If you have a sandbox organization for QA, keep its keys distinct from your production organization's keys even when both are pb_live_. The prefix tells you live vs test, but only the key name and audit log tell you which organization.
Auth error responses
The API returns two error codes for authentication problems. The distinction matters: 401 means the API doesn't know who you are; 403 means it knows, but you can't do that.
401 authentication_required
The key is missing, malformed, expired, or revoked.
{
"error": {
"code": "authentication_required",
"message": "API key is missing or invalid"
}
}
Common causes:
- The
Authorizationheader was omitted entirely. - The header used
TokenorApiKeyinstead ofBearer. - The key was copied with a leading or trailing space.
- The key was revoked or has expired.
- The key was for the wrong environment (e.g. you sent a
pb_test_key to a production-only endpoint — though this normally surfaces as403).
403 permission_denied
The key is valid, but it does not have the scope required for the operation, or the operation targets a resource outside its organization.
{
"error": {
"code": "permission_denied",
"message": "Required scope: observations:write"
}
}
If you see this on an endpoint you expected to work, check the key's scope list in Settings → API. Update the key's scopes (or create a new key with broader scopes), then retry.
Example requests
curl
# Read your session
curl https://api.performanceblocks.com/v1/session \
-H "Authorization: Bearer $PB_API_KEY"
# Create an observation (write scope required)
curl -X POST https://api.performanceblocks.com/v1/observations \
-H "Authorization: Bearer $PB_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"employee_id": "emp_01HXX...",
"type": "strength",
"observation": "Led the Q2 launch retro and surfaced two systemic gaps.",
"observation_date": "2026-04-12"
}'
TypeScript
A small client wrapper that pulls the key from the environment, sets sensible defaults, and surfaces errors:
const BASE_URL = 'https://api.performanceblocks.com/v1';
export class PerfBlocksClient {
constructor(private readonly apiKey: string) {
if (!apiKey?.startsWith('pb_')) {
throw new Error('Missing or malformed Performance Blocks API key');
}
}
async request<T>(
path: string,
init: RequestInit & { idempotencyKey?: string } = {}
): Promise<T> {
const headers = new Headers(init.headers);
headers.set('Authorization', `Bearer
For environments without process.env, pull the key from your platform's secret API instead — for example platform.env.PB_API_KEY on Cloudflare Workers.
Checklist for going to production
Before you flip a workflow from pb_test_ to pb_live_:
- The production key is stored in your secret manager, not in source control or a config file.
- The key has only the scopes the integration needs — nothing more.
- You have a separate key for staging / preview that uses
pb_test_against the sandbox organization. - You have a documented rotation plan and an owner for the key.
- You've subscribed to the Webhooks you need rather than polling.
- Your client respects Rate limits and retries on
429with backoff.