API Keys
Create, list, and revoke the keys that authenticate every other endpoint.
These three routes are session-authenticated, not Bearer-authenticated. They read your Supabase auth cookie. You cannot create, list, or revoke an API key using an API key, so a Authorization: Bearer bp_... header against /api/v1/api-keys will not work. In practice you manage keys in the dashboard at brainpercent.app/chat/developers. The routes below are documented because that page calls them from the browser.
Each user can have a maximum of 10 active keys. Keys follow the format bp_ plus 64 hex characters. They are SHA-256 hashed at rest and shown exactly once, at creation. A lost key cannot be recovered, revoke it and create a new one.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST | /api/v1/api-keys | Session | Create a new API key |
GET | /api/v1/api-keys | Session | List your API keys |
DELETE | /api/v1/api-keys/:id | Session | Revoke an API key |
/api/v1/api-keysCreate a new API key. The full key value is returned only in this response, store it immediately. Session-authenticated, so this must be called from a signed-in browser. Maximum 10 active keys per user.
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Required | Human-readable label for the key (1-100 characters) |
| permissions | string[] | Optional | Permissions to grant: read, write, delete(default: ["read"]) |
| scopes | string[] | Optional | Resource scopes to restrict access: articles, social, projects, user |
| expires_in_days | integer | Optional | Key expiration in days (1-365). Omit for no expiration. |
// Runs in a signed-in browser session. The auth cookie is the credential.
const res = await fetch('https://www.brainpercent.app/api/v1/api-keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
name: 'My App Key',
permissions: ['read', 'write'],
scopes: ['articles', 'social'],
expires_in_days: 90,
}),
});
const { data } = await res.json();
// data.key is the only time you will ever see the full key
console.log('API Key:', data.key);{
"success": true,
"data": {
"id": "b7e2f1a0-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"name": "My App Key",
"key": "bp_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"key_prefix": "bp_a1b2",
"permissions": [
"read",
"write"
],
"scopes": [
"articles",
"social"
],
"created_at": "2026-02-01T12:00:00Z",
"expires_at": "2026-05-02T12:00:00Z"
}
}/api/v1/api-keysList the API keys on your account. Returns masked key prefixes only, never the full key. Session-authenticated.
const res = await fetch('https://www.brainpercent.app/api/v1/api-keys', {
credentials: 'include',
});
const { data } = await res.json();
console.log(`You have ${data.length} API key(s)`);
data.forEach(k => console.log(` ${k.name} (${k.key_prefix}...) active=${k.is_active}`));{
"success": true,
"data": [
{
"id": "b7e2f1a0-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"name": "My App Key",
"key_prefix": "bp_a1b2",
"permissions": [
"read",
"write"
],
"scopes": [
"articles",
"social"
],
"created_at": "2026-01-15T10:00:00Z",
"last_used_at": "2026-02-01T08:30:00Z",
"expires_at": "2026-04-15T10:00:00Z",
"is_active": true
},
{
"id": "c8f3a2b1-4d5e-4f6a-9b0c-1d2e3f4a5b6c",
"name": "CI/CD Pipeline",
"key_prefix": "bp_c3d4",
"permissions": [
"read"
],
"scopes": [
"articles",
"projects"
],
"created_at": "2026-01-20T14:00:00Z",
"last_used_at": null,
"expires_at": null,
"is_active": true
}
]
}/api/v1/api-keys/:idRevoke an API key. Any application using it loses access immediately and the key cannot be restored. Session-authenticated.
const keyId = 'b7e2f1a0-3c4d-4e5f-8a9b-0c1d2e3f4a5b';
const res = await fetch(
`https://www.brainpercent.app/api/v1/api-keys/${keyId}`,
{
method: 'DELETE',
credentials: 'include',
}
);
const result = await res.json();
console.log(result.message);{
"success": true,
"message": "API key revoked"
}Error Shape
These three routes return a flat error object, not the structured envelope the Bearer-authenticated endpoints use:
| Routes | Error body |
|---|---|
| /api/v1/api-keys | { "error": "..." } |
| every Bearer route | { "success": false, "error": { "code", "message" } } |
If you share one error handler across both, branch on whether error is a string or an object.
Permission Reference
| Permission | Allows | Example Endpoints |
|---|---|---|
read | Retrieve resources (GET requests) | GET /articles, GET /projects, GET /user/credits |
write | Create and modify resources (POST requests) | POST /articles/generate, POST /social/generate, POST /social/publish |
delete | Remove resources (DELETE requests) | None of the Bearer endpoints documented here require it. Revoking a key is session-authenticated. |
A key created without an explicit permissions array gets ["read"]. Every write endpoint returns 403 for such a key, which is the most common cause of an unexplained FORBIDDEN.
Scope Reference
| Scope | Endpoints Covered |
|---|---|
articles | /api/v1/articles/*, /api/v1/articles/generate, /api/v1/articles/:id/status |
social | /api/v1/social/content/*, /api/v1/social/generate, /api/v1/social/publish |
projects | /api/v1/projects/* |
user | /api/v1/user/credits, /api/v1/user/usage |
A request is authorized only when the API key has both the required permission AND the matching scope. For example, generating an article requires the write permission and the articles scope.