Authentication
Site Health accepts two kinds of Bearer credentials. Both are sent in the standard Authorization header:
Authorization: Bearer <token>
| Credential | Prefix | Lifetime | Issued by | Used for |
|---|---|---|---|---|
| API key | wsh_ | Never expires (revocable) | User (Settings UI) | REST API + manual MCP client setup |
| OAuth access token | at_ | 30 days (refreshable) | Authorization Server | Claude.ai + DCR-capable MCP clients |
Tokens grant full read access to every site in the issuing user's account. Treat them like passwords.
API keys (wsh_…)
How to create one
- Sign in at
https://sitehealth.octagramlabs.com. - Open Settings → API Keys.
- Click Create key, optionally name it (e.g.
github-actions). - Copy the raw key immediately — Site Health shows it exactly once. After that only the prefix (
wsh_a1b2c3…) is visible.
Under the hood, generateApiKey() produces a 64-hex-char random suffix (wsh_ + 32 random bytes). The key is SHA-256 hashed before storing in Postgres — the plaintext value never touches disk.
Revoking
In the same UI, click Revoke next to a key. Revocation is immediate:
DELETE /api/settings/api-keys?id={apiKeyId}
Programmatic creation (session required)
POST /api/settings/api-keys
Content-Type: application/json
Cookie: session=…
{ "name": "github-actions" }
Response 201:
{
"id": "d1c2b3a4-…",
"keyPrefix": "wsh_a1b2c3d4",
"name": "github-actions",
"createdAt": "2026-04-18T12:00:00.000Z",
"rawKey": "wsh_a1b2c3d4e5f6…"
}
The rawKey field is only present on creation. Save it to your secret store immediately.
Example request with an API key
curl -X POST https://sitehealth.octagramlabs.com/api/external/budget-check \
-H "Authorization: Bearer wsh_a1b2c3d4e5f6…" \
-H "Content-Type: application/json" \
-d '{ "siteId": "11111111-2222-3333-4444-555555555555" }'
OAuth access tokens (at_…)
Issued by Site Health's OAuth 2.1 Authorization Server at POST /api/oauth/token after a Claude.ai (or any DCR-capable) client completes the authorization code + PKCE flow. See OAuth reference for the full flow.
- Lifetime: 30 days (
ACCESS_TOKEN_TTL_MS). - Refresh: yes — use the returned
refresh_tokenwithgrant_type=refresh_token(90-day lifetime). - Revoke:
POST /api/oauth/revokewith{ "token": "at_…" }. - Scopes: currently only
mcp:read.
Tokens are base64url-encoded 32-byte random strings (at_ + 43 chars). Like API keys, they are SHA-256 hashed at rest.
Example request with an OAuth token
curl -X POST https://sitehealth.octagramlabs.com/api/mcp \
-H "Authorization: Bearer at_eyJ0eXAi…" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Where each credential is accepted
| Endpoint | wsh_ | at_ | Session cookie |
|---|---|---|---|
POST /api/external/budget-check | yes | no | no |
POST /api/mcp | yes | yes | no |
GET /api/sites/{siteId}/export | no | no | yes |
POST /api/settings/api-keys | no | no | yes |
/.well-known/oauth-* | — | — | no (public) |
POST /oauth/* token endpoints | — | — | per OAuth spec |
Rate limits
| Endpoint | Limit | Scope |
|---|---|---|
| Scan-trigger endpoints (manual/API scan) | 5 per hour | Per user |
| Everything else (read endpoints, MCP tools, OAuth) | Unlimited | — |
When a scan limit is exceeded Site Health returns:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{ "error": "Scan rate limit exceeded. Try again in 42 minutes." }
Scan deduplication is enforced separately: if a scan for the same site is already queued or running, createAndEnqueueScan() throws SCAN_ALREADY_ACTIVE and the caller gets a 409.
Security notes
- Never commit a raw
wsh_key to version control. GitHub's secret scanning does detect the prefix, but prevention beats detection. - Use short-lived OAuth tokens for anything user-facing. API keys are for server-to-server where rotation is infrequent and audited.
- Site Health logs only the
keyPrefix(first 12 chars) on successful authentication. The raw key is never logged. - All traffic is HTTPS-only (HSTS is enforced at the Vercel edge).
Troubleshooting 401 Unauthorized
- Verify the
Authorization: Bearer …header is present and formatted exactly (single space afterBearer). - Confirm the token has the right prefix (
wsh_for API keys,at_for OAuth). - Check you haven't revoked the key in Settings or let an OAuth token expire past 30 days.
- For MCP 401 responses, the
WWW-Authenticateheader points at/.well-known/oauth-protected-resource— DCR-aware clients will re-register automatically.
Next: OAuth flow →