Skip to main content

Working with files

Premium feature

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 wantOver the API
The agent should know these documents permanentlyYes — upload to a knowledge set
Bulk-move a whole knowledge set between instancesPartly — import yes, but the export archive cannot be downloaded with a key
Transcribe an audio fileYes/v1/audio/transcriptions
Attach a file to one conversationYes — thread-workspace upload
Download a file the agent producedYes — thread-workspace download
Manage the agent's standing filesYes — agent-workspace upload/download
Send an image for the agent to look atNo — 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:

  1. Create a knowledge set (or use an existing one).
  2. Upload files into it.
  3. Attach it to the agent.
  4. Chat — the agent retrieves from it automatically.

Steps 1–3 are management API calls and need management scopes on your key:

StepRouteRequired scope
Create a setPOST /api/knowledge-setsknowledgeset:manage:*
Upload a filePOST /api/knowledge-sets/{id}/knowledge-files/{path}knowledgeset:manage:*
Attach to an agentPOST /api/agents/{id}/knowledge-sets/{ks}/attachagent: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 body is the raw file — not a multipart form

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.

DetailValue
Max size100 MB per file
Virus scanningEvery upload is scanned; an infected file is rejected with 400
ApprovalFiles uploaded this way are auto-approved and queued for ingestion
SubdirectoriesThe 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.

The export archive itself is not downloadable with a key

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"
DetailValue
Formatsflac, m4a, mp3, mp4, mpeg, mpga, oga, ogg, wav, webm
modelOptional; defaults to the speech-to-text alias
ScopeAny 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.

WorkspaceRoutesScope
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 type text are concatenated; image_url and input_audio parts 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.