Projects over the API
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 project | On /api/invoke/{id} |
|---|---|
| Shared workspace (files) | missing — only the agent's or thread's own workspace is reachable |
| The project's tasks, exposed as tools | missing entirely |
| Project-scoped credential contexts | missing |
| The project's model settings and knowledge sets | missing |
agent:chat on /api/invoke/{id} remains the thinner surfaceThis 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.
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 yetA 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-…"
POST /api/tasksTask 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:
| Scope | Reach |
|---|---|
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
| Route | Scope |
|---|---|
GET | project:env:<project_id> |
PUT | project:env-write:<project_id> |
project:env and project:env-write do not imply each otherUnlike 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
| Route | Required 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}/env | project:env:<p> |
PUT /api/projects/{p}/env | project: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 reachable | Why |
|---|---|
| Project members | Changes who reaches the project — browser-session authority, not integration authority. |
| Project invitations | Same 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
| Response | Meaning |
|---|---|
400 | The project id is not a p1… id (or the default alias, which resolves per account and therefore cannot be matched against a scope). |
403 | The 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. |
425 | The project's knowledge set does not exist yet — see above, retry. |
Related docs
- 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