Skip to main content

API Keys

Premium feature

BasePeak.AI uses scoped bearer tokens for programmatic access. A token is tied to the user who created it, carries an explicit list of scopes, and can never do more than its creator could do at the moment it was minted.

Creating a key

The UI lives at Profile → API Keys.

  1. Click Create API key.
  2. Fill in:
    • Name — a short label, shown in the list. Required.
    • Description — optional free text.
    • Expiry — optional. Without one, the key lives until revoked.
    • Scopes — at least one. Use Add scope to choose the type of access and — where relevant — the agent, the credential, or "Any agent"/"Any credential".
  3. Copy the token. It is shown exactly once — only a hash is stored, so a lost token cannot be recovered, only replaced.
Which scopes the dialog offers

The dialog offers the scopes your account is expected to be able to issue: chat and credential:use: scopes always, the file and conversation scopes for any agent you are authorized for (agent:files-write additionally only if you hold the agent:update permission yourself), and a management scope only if you hold the matching permission yourself. Management scopes always apply to every object of their type — the one exception is agent:update, which can be pinned to a single agent. The server always has the final say when you create the key, and reports the reason if it refuses.

For the chat and the file/conversation scopes, the agent picker offers the agents the key can be minted for — including those you reach only through a group grant or a project of your own. System agents and grants pointing at a deleted agent are left out: both would mint, but neither is a sensible target for a key. agent:update follows a different rule: that permission is not tied to a specific agent, so its agent picker lists every agent on the instance.

API access is a premium feature

Programmatic API access is a premium feature. Key creation fails with 403API access is disabled for this instance — when the apiAccess feature is off, and existing keys stop authenticating too. The flag follows your subscription and is not self-service; contact your BasePeak.AI representative if you hit this.

Token format

sk-bpai-<user_id>-<key_id>-<secret>

Only the last segment is secret; the rest identifies which key is being presented. The stored masked form (sk-bpai-1-42-…jvT8) is safe to put in logs and is what the UI shows you after creation.

Using a key

Send it as a bearer token:

curl -sS https://your-instance.platform.basepeak.ai/api/invoke/a17jlm7 \
-H "Authorization: Bearer sk-bpai-1-42-…" \
-H "Content-Type: text/plain" \
--data 'Hello!'

Headers by surface

HeaderWherePurpose
Authorization: Bearer <token>everywhereThe token.
X-BPAI-Agent: <agent>/v1/chat/completions onlyWhich agent to run. Must be covered by the key's scope.
X-BPAI-Thread-Id: <thread>both chat surfacesContinue an existing conversation.
Accept/api/invoke/{id}Selects streaming vs. JSON vs. plain text.

On /api/invoke/{id} the agent comes from the URL path, not a header. See Invoke API and OpenAI-compatible API.

With the OpenAI SDKs

from openai import OpenAI

client = OpenAI(
api_key="sk-bpai-1-42-…",
base_url="https://your-instance.platform.basepeak.ai/v1",
default_headers={"X-BPAI-Agent": "a17jlm7"},
)

resp = client.chat.completions.create(
model="ignored", # the agent's own model is used
messages=[{"role": "user", "content": "Hello!"}],
)
print(resp.choices[0].message.content)
import OpenAI from "openai";

const client = new OpenAI({
apiKey: "sk-bpai-1-42-…",
baseURL: "https://your-instance.platform.basepeak.ai/v1",
defaultHeaders: { "X-BPAI-Agent": "a17jlm7" },
});

const resp = await client.chat.completions.create({
model: "ignored",
messages: [{ role: "user", content: "Hello!" }],
});
console.log(resp.choices[0].message.content);

Scopes

A scope has three segments:

resource:action:id

resource and action come from a fixed list — neither may be a wildcard. Only id may be *.

Chat scopes

ScopeGrants
agent:chat:a17jlm7Chat with that one agent
agent:chat:*Chat with any agent its owner can access
credential:use:<credential>Lets the key use a stored credential — and the tools that need it. See below.

A wildcard chat key is re-checked against its owner's own permissions on every call, so it can never reach an agent its owner could not open in the browser.

Credential scopes decide which tools the agent keeps

This is the most common cause of "the agent works in the browser but ignores its tools over the API".

An agent's tools often need a stored credential — a Google account for Calendar, a token for GitHub. On every API-key call the run is filtered against the key's credential:use:… scopes:

  • credentials the key does not cover are removed from the agent, and
  • every tool that needs one of those credentials is dropped from the run.

This happens silently. There is no error and no warning in the response: the agent simply answers as if the tool did not exist.

Key scopesEffect
agent:chat:<id> onlyEvery credential-backed tool is stripped. The agent can only answer from its instructions and knowledge.
agent:chat:<id> + credential:use:*Nothing is stripped. The agent runs with all its tools.
agent:chat:<id> + credential:use:googleOnly tools whose credentials are covered survive; the rest are dropped.

So if your integration needs the agent's tools, mint the key with credential:use:* (or list the specific credentials):

{ "name": "calendar-bot", "scopes": ["agent:chat:a17jlm7", "credential:use:*"] }

Tools that need no credential are never affected.

Management scopes

Nine further permissions unlock the management API:

ScopeGrants
agent:create:*Create, list, read, copy, export, import agents
agent:update:*Modify agents and their knowledge-set attachments
agent:delete:*Delete agents
knowledgeset:manage:*Knowledge sets, their files, imports and exports
tool-reference:manage:*Tools and MCP servers
model-provider:manage:*Models and model providers
trigger:manage:*Webhooks, email receivers, cronjobs
task:manage:*Tasks and task runs
team:manage:*Agent teams

Management scopes must use * as the id — except agent:update, see below.

Several scopes per key

A key may carry as many scopes as you need. Mixing chat and management scopes on one key is supported and normal:

{
"name": "provisioning-bot",
"scopes": ["agent:chat:*", "agent:create:*", "knowledgeset:manage:*"]
}

At creation time the server checks that you currently hold the agent and management permissions you are asking for. Requesting one you do not have is rejected with 403, which prevents minting a key that would be either dead or an escalation. (credential:use: scopes are deliberately not checked this way.)

Scopes that name no reachable route are, as a rule, rejected outright rather than stored as inert entries — mcp:*, agent:read and admin:* are not valid scopes.

File and conversation scopes

Four further scopes cover files and conversation history. Each is keyed on an agent, so thread:files:a17jlm7 means "the threads of that agent":

ScopeGrants
agent:files:<agent|*>List and download files in the agent's own workspace (read-only). These files seed every new conversation.
agent:files-write:<agent|*>Everything agent:files grants, plus upload and delete. A superset of agent:files — a key does not need both. Can only be minted by an account that itself holds the agent:update permission (see below); a plain chat user cannot self-grant write access to an agent's standing workspace.
thread:files:<agent|*>List, download, upload and delete in a single conversation's workspace (both directions, one scope).
thread:read:<agent|*>List that agent's conversations (GET /api/threads), fetch one (GET /api/threads/{id}) and replay its history (GET /api/threads/{id}/events) — the key account's own conversations only. See Threads.

They are granted like chat scopes: you must be able to reach the agent to mint them. agent:files-write additionally requires you to hold agent:update yourself — minting fails with 403 otherwise, the same way a management scope does when you lack its permission. Beyond that, these scopes are independent of each other and of agent:chat — a chat key does not get file access, and a file key cannot chat.

POST /api/threads/{id}/abort needs no scope of its own: starting a run and cancelling it are the same authority, and agent:chat covers both.

Like management scopes, these can be granted in the Profile → API Keys → Create API key dialog, or created through REST (POST /api/account/apikeys).

Project scopes

Eight further scopes address a project rather than an agent — the unit the browser itself uses for a conversation. Each takes a concrete p1… project id or *:

ScopeGrants
project:chat:<p|*>Start and continue conversations in the project
project:files:<p|*>List and download the project's shared workspace
project:files-write:<p|*>Also upload and delete; a strict superset of project:files
project:knowledge:<p|*>List the project's knowledge set (including ingestion state) and download
project:knowledge-write:<p|*>Also upload and delete; a strict superset of project:knowledge
project:tasks:<p|*>The project's tasks: create, read, update, delete, run, and their runs
project:threads:<p|*>Read the project's conversations: list, fetch and replay them
project:env:<p|*>Read the project's environment variables — values included
project:env-write:<p|*>Set the project's environment variables

project:files-write and project:knowledge-write are strict supersets of their read siblings — a key holding the write half never needs to also mint the read half.

project:env and project:env-write are independent halves

Unlike files and knowledge, the write half here does not imply the read half, or the reverse. project:env reads back the environment variables' stored values — this is where a project's tool secrets live. project:env-write sets them. A key that only needs to rotate a secret should never have to be able to read one — that is why this one scope breaks with the otherwise-general pattern. Details and examples: Projects.

project:tasks is deliberately not split — one scope covers read, write and run alike in the project, mirroring task:manage on the flat surface.

project:threads is not split either, for the opposite reason: it is read-only and has no write half. Editing or deleting a conversation is not on the API-key surface at all, and cancelling a running turn (POST /api/threads/{id}/abort) stays on the chat scope, since starting a run and stopping it are the same authority.

project:chat does not include reading the conversation back

A key holding only project:chat:<p> can start and continue conversations, but cannot list, fetch or replay them — it has to capture the X-Bpai-Thread-Id response header and remember the id itself. Grant project:threads:<p> alongside it so the integration can read its own conversations back.

Reach for project:threads:<p> rather than thread:read:<agent> whenever the intent is "this integration reads its own project": thread:read is keyed to the agent, so it grants every conversation of that agent, across every project it backs. See Conversations.

All nine project scopes ground on project membership alone. Unlike agent:files-write, minting them — including the write halves — requires no management permission: the session path demands nothing more than project access for the same mutations. Full routes, examples and error cases: Projects.

task:manage vs. project:tasks

The two task scopes overlap without either containing the other:

ScopeReach
task:manage (*-only)Every task its owner owns, across all projects — no way to pin it to a single project.
project:tasks:<p>Every task in that one project — including one the owner reaches only through a ThreadAuthorization grant, not through ownership. The flat surface (GET /api/tasks) cannot see this case at all, since it filters on ownership alone.

Task creation also exists only on the project-scoped route — there is no flat POST /api/tasks.

Grounding a key to a single agent

agent:update is the one management permission that accepts a concrete id:

ScopeReach
agent:update:*Any agent its owner can update
agent:update:a17jlm7Only agent a17jlm7403 on any other

Combine it with a matching chat scope to get a key that can talk to one agent and modify that same agent, and nothing else:

{ "name": "self-improve", "scopes": ["agent:chat:a17jlm7", "agent:update:a17jlm7"] }

Grounding narrows only the key. It does not change what you, as its creator, must already hold.

Managing your keys

The list shows each key's name, masked token, creation time, last use and expiry. Last-use is updated asynchronously and at most once a minute per key, so a very recent call may not be reflected immediately.

Revoking takes effect at once — the next request with that token gets 401.

Rotating issues a new secret for the same key: id, name, scopes, expiry and the audit log all stay; the old secret stops working immediately, and the new one is shown exactly once.

Instance administrators additionally see every user's keys under Access → API Keys in the admin area — masked, with owner and audit log — and can revoke any of them. Secrets are never stored or shown; even an administrator can inspect, but never impersonate a user.

REST reference

These routes are for the browser session that owns the keys. API keys cannot mint or manage API keys.

MethodPathDescription
GET/api/account/apikeysList your keys. Never includes the secret.
POST/api/account/apikeysCreate a key. The only response that contains the plaintext token.
GET/api/account/apikeys/{id}Metadata for one key.
GET/api/account/apikeys/{id}/auditThis key's 50 most recent audit rows. Not paginated.
DELETE/api/account/apikeys/{id}Revoke. Returns 204; a repeat call returns 404.
POST/api/account/apikeys/{id}/rotateNew secret for the same key; the response contains it exactly once.

Create request:

{
"name": "ci-pipeline",
"description": "Release notes generator",
"scopes": ["agent:chat:a17jlm7"],
"expiresAt": "2026-12-31T23:59:59Z"
}

Create response — note key, which you will not see again:

{
"id": 42,
"userId": "1",
"name": "ci-pipeline",
"description": "Release notes generator",
"key": "sk-bpai-1-42-sJ_jvT…",
"maskedKey": "sk-bpai-1-42-...jvT8",
"scopes": ["agent:chat:a17jlm7"],
"createdAt": "2026-06-05T12:34:56Z",
"expiresAt": "2026-12-31T23:59:59Z"
}

At least one scope is required.

Audit trail

Every successful call made with a key is recorded: which key and agent, the route, the resulting status code and how long it took. Retention defaults to 90 days. Review the 50 most recent entries via GET /api/account/apikeys/{id}/audit.

Security model

ItemHandling
The token itselfNever stored. Shown once, then only its hash is kept.
Secret at restbcrypt-hashed.
The Authorization headerStripped immediately after authentication, so it never reaches request handlers or their logs.
Route reachA key is confined to an allowlist — chat, invoke, the agent and thread files, the thread routes (list, get, history, abort) and the management routes. Everything else returns 403, even if scopes might suggest otherwise.
Owner re-checkWildcard chat keys are re-authorized against their owner's current access on every call.
RevocationImmediate.

Two consequences worth designing around: revoking a key is instant and needs no cache flush, and a key's reach shrinks with its owner's — losing access to an agent disables wildcard keys for that agent right away.

Troubleshooting

ResponseMeaningWhat to check
401 auth/bad-token-formatThe bearer is not a well-formed sk-bpai-… tokenTruncation in an environment variable, or a leftover placeholder value. Compare against the masked token in the UI.
401 auth/invalid-tokenWell-formed but not acceptedMost often revoked; otherwise a mistyped secret. Since - is valid in the secret, a bad copy will not always change its length.
401 auth/key-expiredPast the expiry dateThe response carries expiredAt so you can surface it. Mint a replacement.
403 auth/api-access-disabledapiAccess is off for the instanceNot self-service — contact your BasePeak.AI representative.
403 authz/scope-missingScopes do not cover this request. On /v1/chat/completions and /api/invoke/{id}, the body names the requiredScope; the file routes (/api/agents/{id}/files…, /api/threads/{id}/files…) and the thread routes (GET /api/threads, GET /api/threads/{id}, GET /api/threads/{id}/events, POST /api/threads/{id}/abort) return a plain 403 with no requiredScope field.For /v1, check X-BPAI-Agent matches an agent:chat:<id> scope; for /api/invoke/{id}, the agent in the path. For the thread routes, check the key carries either thread:read for the conversation's agent or project:threads for its project — or agent:chat when aborting.
403 on a route you expectedThe route is not on the API-key allowlistListing threads, reading one, replaying its history and aborting an in-flight turn all work with a key — renaming (PUT) and deleting (DELETE) a thread do not. See API Overview.
404 model_not_foundThe agent or alias does not resolveA misspelled ID, or an alias that does not exist on this instance.

Threads from a key appear in an "API" project

Expected. Only /v1/chat/completions derives a thread name; threads created through /api/invoke/{id} stay unnamed. The admin thread list shows the literal "API" as the project because the key carries an agent scope rather than a project scope — these threads are intentionally not attached to a project. See Threads.

That holds for /api/invoke/{id} and /v1/chat/completions. A thread started through POST /api/projects/{project_id}/invoke is the exception: it belongs to the project and appears there like any other conversation. See Projects.