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