Working with files
How you get a document to an agent over the API depends on whether the agent should know the document permanently or just look at it for one conversation.
Read the summary table first — it shows the fastest path for each case.
| What you want | Over the API |
|---|---|
| The agent should know these documents permanently | Yes — upload to a knowledge set |
| Bulk-move a whole knowledge set between instances | Partly — import yes, but the export archive cannot be downloaded with a key |
| Transcribe an audio file | Yes — /v1/audio/transcriptions |
| Attach a file to one conversation | Yes — thread-workspace upload |
| Download a file the agent produced | Yes — thread-workspace download |
| Manage the agent's standing files | Yes — agent-workspace upload/download |
| Send an image for the agent to look at | No — the chat surfaces are text-only |
Knowledge sets: the supported path
A knowledge set is a collection of documents that gets ingested, embedded and made searchable to any agent it is attached to. This is the API-supported way to give an agent documents.
The flow is:
- Create a knowledge set (or use an existing one).
- Upload files into it.
- Attach it to the agent.
- Chat — the agent retrieves from it automatically.
Steps 1–3 are management API calls and need management scopes on your key:
| Step | Route | Required scope |
|---|---|---|
| Create a set | POST /api/knowledge-sets | knowledgeset:manage:* |
| Upload a file | POST /api/knowledge-sets/{id}/knowledge-files/{path} | knowledgeset:manage:* |
| Attach to an agent | POST /api/agents/{id}/knowledge-sets/{ks}/attach | agent:update:* or agent:update:{id} |
A chat-only key (agent:chat:…) cannot do any of this — it can only talk
to an agent whose knowledge sets are already in place.
Uploading a file
The filename comes from the URL path, and the request body is the
file's raw bytes. Do not build a multipart/form-data request here.
curl -sS -X POST \
"https://your-instance.platform.basepeak.ai/api/knowledge-sets/ks1abc/knowledge-files/q3-report.pdf" \
-H "Authorization: Bearer sk-bpai-1-42-…" \
-H "Content-Type: application/pdf" \
--data-binary @q3-report.pdf
Returns 201 Created with the knowledge-file object.
| Detail | Value |
|---|---|
| Max size | 100 MB per file |
| Virus scanning | Every upload is scanned; an infected file is rejected with 400 |
| Approval | Files uploaded this way are auto-approved and queued for ingestion |
| Subdirectories | The path is a trailing wildcard, so …/knowledge-files/2026/q3/report.pdf works |
Ingestion is asynchronous — the upload returning 201 means the file is
stored, not that it is searchable yet. Poll the file list and watch its
state if you need to know when it is ready.
import pathlib, httpx
path = pathlib.Path("q3-report.pdf")
r = httpx.post(
f"{BASE}/api/knowledge-sets/ks1abc/knowledge-files/{path.name}",
headers={"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/pdf"},
content=path.read_bytes(), # raw bytes, no multipart
timeout=120,
)
r.raise_for_status()
print(r.json())
Listing and deleting
# List
curl -sS "$BASE/api/knowledge-sets/ks1abc/knowledge-files" \
-H "Authorization: Bearer $TOKEN"
# Delete
curl -sS -X DELETE \
"$BASE/api/knowledge-sets/ks1abc/knowledge-files/q3-report.pdf" \
-H "Authorization: Bearer $TOKEN"
Both need knowledgeset:manage:*.
Bulk import and export
For moving a whole knowledge set — between instances, or as a backup — use the transfer routes rather than uploading file by file.
# Export: returns a job you then poll.
curl -sS -X POST "$BASE/api/knowledge-sets/ks1abc/exports" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -d '{}'
# Import: this one IS a multipart form, field name "file".
curl -sS -X POST "$BASE/api/knowledge-set-imports" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@knowledge-set.zip"
Note the asymmetry with single-file upload: imports are
multipart/form-data with a form field called file, while single-file
uploads are raw-body. Imports are size-capped by instance configuration and
return 413 when the archive is too large.
Creating, listing and deleting exports works, but
GET /api/knowledge-sets/{id}/exports/{export_id}/download is not on the
API-key allowlist — fetching the bytes needs a browser session. So an export
can be produced over the API but not collected over it.
Audio transcription
The one binary-input route on the chat side is OpenAI-compatible and multipart, exactly as the OpenAI SDKs send it:
curl -sS -X POST "$BASE/v1/audio/transcriptions" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@meeting.mp3" \
-F "model=speech-to-text"
| Detail | Value |
|---|---|
| Formats | flac, m4a, mp3, mp4, mpeg, mpga, oga, ogg, wav, webm |
model | Optional; defaults to the speech-to-text alias |
| Scope | Any valid key — this route is not scope-gated |
Anything else returns 400 with the allowed-format list.
Conversation, agent and project files
Three workspaces are reachable with a key. All three take the raw file bytes as the request body, with the filename in the URL path — the same convention as knowledge upload, not a multipart form.
| Workspace | Routes | Scope |
|---|---|---|
The agent's own — read (GET) | /api/agents/{id}/files, /api/agents/{id}/files/{path} | agent:files or agent:files-write |
The agent's own — write (POST, DELETE) | /api/agents/{id}/files/{path} | agent:files-write |
| One conversation's (both directions) | /api/threads/{id}/files, /api/threads/{id}/files/{path} | thread:files |
| A project's (both directions) | /api/projects/{project_id}/files, /api/projects/{project_id}/files/{path} | project:files (read) or project:files-write (read + write) |
GET, POST and DELETE are supported on the {path} form; GET on the bare
/files form lists. The agent workspace splits read from write:
agent:files-write is a superset of agent:files (it covers both), and
minting it requires the agent:update permission — see
API keys. The thread
workspace has no such split; thread:files covers both directions. The
project workspace splits read from write again, like the agent's
(project:files-write is a strict superset of project:files) — but unlike
agent:files-write, minting it needs nothing beyond project membership.
See Projects.
Which one you want depends on lifetime and reach. The agent's workspace
is copied into every new conversation, so put standing files there that
belong to no particular project. A thread's workspace belongs to that
one conversation, so put a document there when you want the agent to look at
it once. A project's workspace sits in between: visible to every
conversation that starts in the project (POST /api/projects/{project_id}/invoke), but not to conversations outside it.
The round trip: upload with a key, then ask about it in that same conversation.
# Upload a file into a project's shared workspace.
curl -sS -X POST "$BASE/api/projects/p1abc/files/report.csv" \
-H "Authorization: Bearer $TOKEN" \
--data-binary @report.csv
# Start a new conversation in that same project and ask about it.
curl -sS -X POST "$BASE/api/projects/p1abc/invoke" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: text/plain" \
--data 'What does report.csv say?'
# Give the agent a document for this conversation only.
curl -sS -X POST "$BASE/api/threads/t1abcde/files/contract.pdf" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/pdf" \
--data-binary @contract.pdf
# Collect something the agent produced.
curl -sS "$BASE/api/threads/t1abcde/files/summary.md" \
-H "Authorization: Bearer $TOKEN" -o summary.md
# List what is in the conversation's workspace.
curl -sS "$BASE/api/threads/t1abcde/files" -H "Authorization: Bearer $TOKEN"
A key only ever reaches its own owner's conversations, and only those of an agent its scope covers.
What is not available over the API
Images and other non-text content in chat
Both chat surfaces are text-only:
- On
/v1/chat/completions, structured content parts of typetextare concatenated;image_urlandinput_audioparts are silently skipped — no error, they simply do not reach the agent. - On
/api/invoke/{id}, the body is the prompt text.
Do not rely on an image in a messages array reaching the model.
Working around the remaining gaps
For what is still missing — sending an image, or moving a whole set of documents in bulk — these patterns are the practical fallbacks:
1. Put the document in a knowledge set. Best when the same documents serve many conversations — policies, product docs, manuals. The agent retrieves what it needs per question.
2. Inline the text in the prompt. For a one-off document, extract the
text yourself and send it as part of the prompt on
/api/invoke/{id}. Bounded by the model's context
window, but it needs no file plumbing at all:
document = pathlib.Path("contract.txt").read_text()
prompt = f"Review this contract and list unusual clauses:\n\n{document}"
httpx.post(f"{BASE}/api/invoke/a17jlm7",
headers={"Authorization": f"Bearer {TOKEN}",
"Accept": "application/json",
"Content-Type": "text/plain"},
content=prompt, timeout=None)
3. Let a tool move the bytes. When the output needs to land somewhere outside BasePeak.AI, give the agent a tool that delivers the artefact directly — mail it, push it to storage, post it to a webhook — rather than round-tripping it through the thread workspace. This also sidesteps having to poll for completion.
4. Use a per-conversation knowledge set. If a conversation needs the agent to search across a larger set of documents rather than just read one, create a knowledge set for it, attach it, and delete it afterwards. Heavier than a thread-workspace upload, but it gives you retrieval instead of a flat file.
Related docs
- Management API — knowledge-set CRUD and scopes
- Invoke API — sending prompts
- Threads — conversation state
- Projects — a project's workspace in detail