Skip to main content

Projects over the API

Premium feature

The unit of conversation in BasePeak.AI is the project, not the bare agent. A project bundles a shared workspace, its own tasks, project-scoped credentials and its own model settings — everything that sets a browser conversation apart from one started through /api/invoke/{id}.

POST /api/projects/{project_id}/invoke parents the new thread onto the project — the same parent link the UI itself sets. From that point on it is the same conversation that appears in the browser, not merely a similar one.

Comes from the projectOn /api/invoke/{id}
Shared workspace (files)missing — only the agent's or thread's own workspace is reachable
The project's tasks, exposed as toolsmissing entirely
Project-scoped credential contextsmissing
The project's model settings and knowledge setsmissing
agent:chat on /api/invoke/{id} remains the thinner surface

This page does not obsolete /api/invoke/{id} — it remains the right choice for headless agents and system agents that belong to no project. It is just thinner: it talks to the bare agent, not to the conversation the browser runs. Address the project when you need its context; address the agent when you do not.

A project is addressed by its project id, always the p1… form — everywhere, for the conversation and for every piece of project content. The assistant-scoped path the browser uses (/api/assistants/{id}/projects/{project_id}/…) stays session-only; a key reaches only /api/projects/{project_id}/….

# start a conversation in a project
curl -X POST https://<host>/api/projects/p1abc/invoke \
-H "Authorization: Bearer $BPAI_TOKEN" \
-H "Content-Type: text/plain" \
--data 'Summarise the files in this project'

# upload a file the next conversation in this project can read
curl -X POST https://<host>/api/projects/p1abc/files/report.csv \
-H "Authorization: Bearer $BPAI_TOKEN" \
--data-binary @report.csv

Chatting inside a project​

POST /api/projects/{project_id}/invoke
POST /api/projects/{project_id}/invoke/thread/{thread_id}
POST /api/projects/{project_id}/invoke/threads/{thread_id}

Scope: project:chat:<project_id>.

Both routes behave like their counterpart on /api/invoke/{id}: the same response body, the same X-BPAI-Thread-Id response header, the same Accept variants (streaming/JSON/plain text), and the same ?async=true. A client already talking to /api/invoke/{id} changes only the URL.

# New conversation in the project.
curl -sS -X POST \
https://your-instance.platform.basepeak.ai/api/projects/p1abc/invoke \
-H "Authorization: Bearer sk-bpai-1-42-…" \
-H "Accept: application/json" \
-H "Content-Type: text/plain" \
--data 'Summarise the files in this project.'

# Continue that same conversation.
curl -sS -X POST \
https://your-instance.platform.basepeak.ai/api/projects/p1abc/invoke/thread/t1abcde \
-H "Authorization: Bearer sk-bpai-1-42-…" \
-H "Accept: application/json" \
--data 'And what does that mean for Q4?'

# Without waiting for the reply.
curl -sS -X POST \
'https://your-instance.platform.basepeak.ai/api/projects/p1abc/invoke?async=true' \
-H "Authorization: Bearer sk-bpai-1-42-…" \
--data 'Draft the monthly report.'

The agent comes from the project — you never name one. A {thread_id} belonging to another project is never silently adopted: the route answers 403. See Threads for continuation, polling and async in full; none of that changes here.

An ordinary conversation, not a special case

A thread created through this route is an ordinary chat thread. The existing flat thread routes (GET /api/threads/{id}, /events, /files, POST .../abort) keep working for it, unchanged. Only the project itself — the parent thread — stays unreachable there; its own workspace has its own scopes, below.

For reading those conversations back, prefer project:threads:<p>: it covers the list, fetch and replay routes for this project's conversations only. The agent-keyed thread:read:<agent> also works, but grants every conversation of the project's agent, across every project it backs — wider than the project you are integrating with. /files still keys on the agent (thread:files), and POST .../abort on agent:chat. See Conversations.

Files: the project's shared workspace​

GET    /api/projects/{project_id}/files
GET /api/projects/{project_id}/file[s]/{file}
POST /api/projects/{project_id}/file[s]/{file}
DELETE /api/projects/{project_id}/file[s]/{file}

Scope: project:files:<project_id> (read) or project:files-write:<project_id> (read and write — -write is a strict superset of project:files, so a key never needs both).

This is the same shared workspace the UI shows as the project's files — not the workspace of a single thread. Every conversation that starts in the project sees it.

# List.
curl -sS https://your-instance.platform.basepeak.ai/api/projects/p1abc/files \
-H "Authorization: Bearer sk-bpai-1-42-…"

# Upload — raw bytes, not a multipart form.
curl -sS -X POST \
https://your-instance.platform.basepeak.ai/api/projects/p1abc/files/report.csv \
-H "Authorization: Bearer sk-bpai-1-42-…" \
--data-binary @report.csv

# Download.
curl -sS https://your-instance.platform.basepeak.ai/api/projects/p1abc/files/report.csv \
-H "Authorization: Bearer sk-bpai-1-42-…" -o report.csv

# Delete.
curl -sS -X DELETE \
https://your-instance.platform.basepeak.ai/api/projects/p1abc/files/report.csv \
-H "Authorization: Bearer sk-bpai-1-42-…"

A document uploaded with project:files-write is automatically visible to the next conversation in the same project — the same guarantee Files describes for the shared workspace.

Knowledge: upload, then poll for ingestion state​

GET    /api/projects/{project_id}/knowledge
GET /api/projects/{project_id}/knowledge/{file}
POST /api/projects/{project_id}/knowledge/{file}
DELETE /api/projects/{project_id}/knowledge/{file}

Scope: project:knowledge:<project_id> (read) or project:knowledge-write:<project_id> (read and write — also a strict superset).

There is no separate status route. GET /api/projects/{project_id}/knowledge returns each file's state, error, and ingestion start/end time — the same path the UI itself takes: upload, then poll the list until state settles.

# Upload.
curl -sS -X POST \
https://your-instance.platform.basepeak.ai/api/projects/p1abc/knowledge/handbook.pdf \
-H "Authorization: Bearer sk-bpai-1-42-…" \
-H "Content-Type: application/pdf" \
--data-binary @handbook.pdf

# Poll until the file is ingested.
curl -sS https://your-instance.platform.basepeak.ai/api/projects/p1abc/knowledge \
-H "Authorization: Bearer sk-bpai-1-42-…"
import httpx, time

http = httpx.Client(base_url=BASE, headers={"Authorization": f"Bearer {TOKEN}"})

with open("handbook.pdf", "rb") as f:
http.post("/api/projects/p1abc/knowledge/handbook.pdf",
headers={"Content-Type": "application/pdf"},
content=f.read())

while True:
files = http.get("/api/projects/p1abc/knowledge").json()["items"]
entry = next((f for f in files if f["fileName"] == "handbook.pdf"), None)
if entry and entry["state"] not in ("pending", "ingesting"):
break
time.sleep(2)
425 Too Early while the knowledge set does not exist yet

A newly created project gets its knowledge set asynchronously. Upload right after that, and POST and DELETE on this route answer 425 Too Early until it is ready. A client scripting an upload immediately after project creation will hit this reliably — treat 425 as "not yet", not as an error, and retry the call.

Tasks: creation exists only here​

POST   /api/projects/{project_id}/tasks
GET /api/projects/{project_id}/tasks
GET /api/projects/{project_id}/tasks/{id}
PUT /api/projects/{project_id}/tasks/{id}
DELETE /api/projects/{project_id}/tasks/{id}
POST /api/projects/{project_id}/tasks/{id}/run
POST /api/projects/{project_id}/tasks/{id}/runs/{run_id}/steps/{step_id}/run
GET /api/projects/{project_id}/tasks/{id}/runs
GET /api/projects/{project_id}/tasks/{id}/runs/{run_id}
DELETE /api/projects/{project_id}/tasks/{id}/runs/{run_id}
POST /api/projects/{project_id}/tasks/{id}/runs/{run_id}/abort

Scope: project:tasks:<project_id> — unsplit, covers read, write and run alike.

# Create a task.
curl -sS -X POST https://your-instance.platform.basepeak.ai/api/projects/p1abc/tasks \
-H "Authorization: Bearer sk-bpai-1-42-…" \
-H "Content-Type: application/json" \
-d '{"name": "Weekly report", "steps": [{"step": "Summarise the week"}]}'

# Run it.
curl -sS -X POST \
https://your-instance.platform.basepeak.ai/api/projects/p1abc/tasks/w1abc123/run \
-H "Authorization: Bearer sk-bpai-1-42-…" \
-H "Content-Type: application/json" -d '{}'

# Read its runs.
curl -sS https://your-instance.platform.basepeak.ai/api/projects/p1abc/tasks/w1abc123/runs \
-H "Authorization: Bearer sk-bpai-1-42-…"
There is no flat POST /api/tasks

Task creation works only through this project-scoped route. The flat surface under Management API can read, update and run an existing task (scope task:manage), but never create a new one.

task:manage and project:tasks are not superset/subset​

The two overlap without either containing the other:

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

A key holding project:tasks:p1abc therefore reaches tasks in a project merely shared with its owner — something no task:manage key ever could, regardless of its reach.

Environment variables: two independent halves​

GET /api/projects/{project_id}/env
PUT /api/projects/{project_id}/env
RouteScope
GETproject:env:<project_id>
PUTproject:env-write:<project_id>
project:env and project:env-write do not imply each other

Unlike files and knowledge, the write half here is not a superset of the read half. GET /api/projects/{project_id}/env returns the environment variables' stored values — the project's tool secrets live there. A key holding only project:env-write can set values but not read them; a key holding only project:env can read them but not change them. A caller that needs both must be minted with both scopes.

The reason for breaking with the usual pattern: a key that only needs to rotate a secret should never have to be able to read one.

# Set values.
curl -sS -X PUT https://your-instance.platform.basepeak.ai/api/projects/p1abc/env \
-H "Authorization: Bearer sk-bpai-1-42-…" \
-H "Content-Type: application/json" \
-d '{"API_TOKEN": "sk-live-…"}'

# Read values back — requires project:env, not project:env-write.
curl -sS https://your-instance.platform.basepeak.ai/api/projects/p1abc/env \
-H "Authorization: Bearer sk-bpai-1-42-…"

Scopes at a glance​

RouteRequired scope
POST /api/projects/{p}/invoke, .../invoke/thread(s)/{thread}project:chat:<p>
GET /api/projects/{p}/files, .../file(s)/{file}project:files:<p> or project:files-write:<p>
POST/DELETE /api/projects/{p}/file(s)/{file}project:files-write:<p>
GET /api/projects/{p}/knowledge, .../knowledge/{file}project:knowledge:<p> or project:knowledge-write:<p>
POST/DELETE /api/projects/{p}/knowledge/{file}project:knowledge-write:<p>
POST/GET/PUT/DELETE /api/projects/{p}/tasks…project:tasks:<p>
GET /api/projects/{p}/envproject:env:<p>
PUT /api/projects/{p}/envproject:env-write:<p>

All eight scopes ground on project membership alone — no management permission is needed to mint any of them, including the write halves: the browser session demands nothing more than project access for the same mutations, and these scopes demand no more. That sets them apart from agent:files-write, whose minting additionally requires agent:update — see API Keys.

A wildcard key (project:chat:* and so on) is re-checked against its owner's current project membership on every call — if the owner loses access to a project, such a scope stops working for that project immediately.

What still isn't reachable over the API​

Three project surfaces stay closed to keys, not by oversight but because they change who may reach a project rather than driving it:

Not reachableWhy
Project membersChanges who reaches the project — browser-session authority, not integration authority.
Project invitationsSame reasoning: a compromised key should not be able to invite anyone into the project.
Project credentials (management)Hands out stored secrets — unlike project:env, which exists specifically for that purpose.

Troubleshooting​

ResponseMeaning
400The project id is not a p1… id (or the default alias, which resolves per account and therefore cannot be matched against a scope).
403The scopes do not cover this project, or the key's owner no longer has access to it. A scope for project A never covers project B.
425The project's knowledge set does not exist yet — see above, retry.
  • API Keys — the nine project scopes in context with every other scope
  • Invoke API — the event stream both routes share
  • Threads — continuation, polling, async
  • Files — the project workspace beside the agent's and the thread's