Files
Upload a file once through the OpenAI-compatible Files API, then attach it to as many agent turns as you need, or ingest it into a knowledge base without a second upload.
How It Works
POST /v1/files stores the bytes and answers a file object with an id such as file_607fe7c92e2b4d599d8f7b1a789dddd8. From then on you pass the id, never the bytes: an input_file part on /v1/responses attaches the file to a turn, and the knowledge-base attach endpoint ingests it as a document.
Every file has a purpose, and the purpose decides its lifecycle:
purpose | For | Lifetime | Deletable here |
|---|---|---|---|
agent_input | Attaching to agent turns | Expires 24 hours after it was last used in a turn | Yes |
agent_output | Files an agent's shell wrote to outputs/; created by the server only | Kept for 30 days from creation; a turn that names it does not extend that | Yes, by the person who asked for it and by organization admins |
knowledge | Ingesting into a knowledge base | Never expires; belongs to the knowledge document | No, 409 file_in_use |
There is no format allowlist at upload: any file up to the size cap is stored. Whether a file is useful is decided where it is used — a tool that cannot read a format refuses it when it is called.
Uploading a File
POST /v1/files
A multipart/form-data request with two fields, file and purpose. purpose is read as a request parameter, so the query form the API reference lists, POST /v1/files?purpose=agent_input, works as well as the form field shown here.
curl -X POST https://api.2kw.ai/v1/files \
-H "Authorization: Bearer sk_your_api_key" \
-F "purpose=agent_input" \
-F "file=@quote.eml;type=message/rfc822"
{
"id": "file_607fe7c92e2b4d599d8f7b1a789dddd8",
"object": "file",
"bytes": 84,
"created_at": 1790322596,
"expires_at": 1790408996,
"filename": "quote.eml",
"purpose": "agent_input"
}
created_at and expires_at are Unix seconds. expires_at is null for a knowledge file, which never expires. The stored filename is the uploaded name with any directory part, control characters and anything past 255 characters removed.
A purpose other than agent_input or knowledge is refused. That includes agent_output: those files are written by an agent's sandbox, never uploaded.
{
"error": {
"message": "Invalid or not permitted purpose: assistants",
"type": "invalid_request_error",
"param": "purpose",
"code": "invalid_purpose"
}
}
Retention
An agent_input file lives for 24 hours from its last use. Every /v1/responses request that names the file in its own input resets expires_at to 24 hours from that moment, so a file you keep attaching never runs out.
Only the current request's files are renewed
A file named in the conversation's history — an earlier turn replayed through previous_response_id or a conversation — is shown to the model again but is not renewed. An attachment from the start of a long conversation therefore lapses 24 hours after the turn that last sent it, even while the conversation goes on. The model then sees it marked as no longer available. Send the id again, or upload the file again, to bring it back.
Once a file is past expires_at or deleted, every read of it answers 410 file_expired. A background sweep runs every 15 minutes: it marks lapsed files deleted and removes the bytes of files that have been deleted for more than 7 days. Until the sweep has run, a lapsed file still appears in GET /v1/files while GET /v1/files/{fileId} already answers 410.
A knowledge file has no expiry. Its lifecycle belongs to the knowledge-base document it was ingested into; deleting the document is how it goes away.
Reading and Deleting
GET /v1/files/{fileId}
Returns the file object, in the same shape as the upload response.
GET /v1/files/{fileId}/content
Streams the bytes as a download:
HTTP/1.1 200
Content-Type: message/rfc822
Content-Length: 84
Content-Disposition: attachment; filename="=?UTF-8?Q?quote.eml?="; filename*=UTF-8''quote.eml
X-Content-Type-Options: nosniff
Content-Type is the type declared at upload, or application/octet-stream when none was. The response is always an attachment with nosniff, so an uploaded HTML or SVG file is downloaded, never rendered.
GET /v1/files/{fileId}/preview
Serves a raster preview of a file, for the caller that requested it. The answer is the file itself when its first bytes say it is a PNG, JPEG, WebP or GIF of at most 20 MB, and otherwise its <file>.preview.png companion, if the sandbox wrote one beside it. The type is decided by the bytes, never by the file's name or its stored Content-Type. A file that is neither, a .png whose bytes are HTML included, answers 404 with the code no_preview.
The response is an attachment with nosniff and Content-Security-Policy: default-src 'none'; sandbox, so a preview is never rendered on the API's origin.
Held files
An agent whose shell is set to outputs: held (see "Delivered files" under Tools) writes agent_output files that are held until the person who asked for them approves handing them over. A held file:
- cannot be downloaded:
GET /v1/files/{fileId}/contentanswers409 file_held, for the requester and for an admin alike; - is visible only to the person who asked for it.
GET /v1/files/{fileId}and the listing show it to them and answer404to everyone else, and/previewanswers that person and no one else; - can be deleted by that person and by organization admins, but an admin cannot read it;
- cannot be named in an
input_filefrom another conversation: the request is refused with409 file_heldbefore the model runs.
{
"error": {
"message": "File file_607fe7c92e2b4d599d8f7b1a789dddd8 is held until its requester delivers it",
"type": "invalid_request_error",
"param": "id",
"code": "file_held"
}
}
The agent reads its own held files freely inside the conversation that produced them. A held file's name and size are visible to anyone who can read that conversation, because the tool result that reports them is part of it. Delivering the files ends the hold: from then on /content answers 200.
DELETE /v1/files/{fileId}
Deletes an agent_input file:
{ "id": "file_607fe7c92e2b4d599d8f7b1a789dddd8", "object": "file", "deleted": true }
The delete takes effect at once — reads and /v1/responses answer 410 file_expired from then on — and the bytes are removed 7 days later. A knowledge file cannot be deleted here:
{
"error": {
"message": "File file_4e8282efb2f94ea2a9a33e43f513a58e belongs to a knowledge base; delete the document instead",
"type": "invalid_request_error",
"param": null,
"code": "file_in_use"
}
}
Not found versus gone
404 means the id is unknown to you: it covers an id that never existed, a file of another organization, and a file uploaded by another end user of an embedded panel. All three answer the same body, so a 404 never reveals that a file exists. 410 file_expired means the file was yours and has been deleted or has passed its lifetime.
{
"error": {
"message": "No such file: file_00000000000000000000000000000000",
"type": "invalid_request_error",
"param": "id",
"code": null
}
}
Listing Files
GET /v1/files
Lists the organization's files that are not deleted, newest first.
| Query parameter | Meaning |
|---|---|
purpose | agent_input, agent_output or knowledge. Any other value answers 400 invalid_purpose. A held agent_output file is listed only for the person who asked for it |
page | Zero-based page number. Default 0 |
size | Page size. Default 20 |
curl "https://api.2kw.ai/v1/files?purpose=agent_input&size=2" \
-H "Authorization: Bearer sk_your_api_key"
The list is a page object, not OpenAI's { "object": "list", "data": [...] } envelope — read the files from content:
{
"content": [
{
"id": "file_607fe7c92e2b4d599d8f7b1a789dddd8",
"object": "file",
"bytes": 84,
"created_at": 1790322596,
"expires_at": 1790408996,
"filename": "quote.eml",
"purpose": "agent_input"
},
{
"id": "file_aad6de1928cc4966886107337d747c94",
"object": "file",
"bytes": 84,
"created_at": 1790322587,
"expires_at": 1790408987,
"filename": "quote.eml",
"purpose": "agent_input"
}
],
"number": 0,
"size": 2,
"numberOfElements": 2,
"totalElements": 2,
"totalPages": 1,
"first": true,
"last": true,
"empty": false
}
The response also carries pageable and sort objects. The list covers the whole organization; it is not available to chat-only users or to the embedded panel (see Permissions).
Limits and Quotas
| Limit | Value | Applies to | Error |
|---|---|---|---|
| File size | 25 MB | Every upload | 413 file_too_large |
| Active files | 50 agent_input files not yet deleted | Each end user of an embedded panel | 429 file_quota_exceeded |
| Upload volume | 200 MB in any rolling 24 hours | Each end user of an embedded panel | 429 file_quota_exceeded |
| File references | 100 distinct file_ids per /v1/responses request | Every turn | 400 invalid_file |
Uploads with an API key or a signed-in member count against the size cap only; the two quotas bound each end user of an embedded panel separately.
{
"error": {
"message": "File is 27000000 bytes; the limit is 26214400 bytes",
"type": "invalid_request_error",
"param": "file",
"code": "file_too_large"
}
}
A 429 carries "type": "rate_limit_error". An end user frees quota by deleting files or waiting for them to expire; the rolling byte budget frees up as uploads age past 24 hours.
Using a File in a Turn
Reference an agent_input file with an input_file part in a message on POST /v1/responses:
{
"type": "message",
"role": "user",
"content": [
{ "type": "input_text", "text": "Create an inquiry from this email." },
{ "type": "input_file", "file_id": "file_607fe7c92e2b4d599d8f7b1a789dddd8" }
]
}
Only file_id is accepted. file_data and file_url are refused with 400 unsupported_file_source: upload the bytes first, then reference the id.
Every file is checked before the turn runs. Each file_id in the request must be a well-formed id, belong to your organization (and to the same end user, for a panel caller) and be live. The first one that fails ends the request with an error and nothing is sent to the model, so a bad reference costs no tokens:
{
"error": {
"message": "File file_607fe7c92e2b4d599d8f7b1a789dddd8 has expired or was deleted",
"type": "invalid_request_error",
"param": null,
"code": "file_expired"
}
}
The files that pass are renewed for another 24 hours (see Retention).
What the model sees
Each attached file becomes one line after the message's text, with filename, type, size and id:
Create an inquiry from this email.
[Attached file: quote.eml · message/rfc822 · 84 B · id=file_607fe7c92e2b4d599d8f7b1a789dddd8]
The model gets the metadata, not the file's contents. A file from the conversation's history that has lapsed since is rendered as [Attached file: quote.eml · id=file_… · no longer available], so the model does not try to use it.
An attached image (image/jpeg, image/png, image/webp or image/gif, up to 20 MB) additionally reaches the model as a picture, next to its line, so the model can describe or read it. The ten most recent images of a conversation are sent this way on every turn; older ones, larger files and other types keep the line only. If the model does not accept image input, the turn is retried once with the line only and the model is told it cannot see images.
Handing a file to a tool
Whenever a turn carries files, the model is told it can hand a file's bytes to a tool by setting a base64 argument to the string backbone:file:{id} — the whole argument value, nothing around it.
Where that placeholder is replaced depends on where the tool runs:
| Tool | Receives |
|---|---|
| A relay tool of an embedded panel | The file's bytes, base64. The panel fetches them with GET /v1/files/{fileId}/content and substitutes them before the call reaches your page |
| A function tool (your webhook), an MCP connector, a client-executed tool in your own client | The literal string backbone:file:{id}. Fetch the bytes yourself with GET /v1/files/{fileId}/content if you need them |
The stored tool call always keeps the placeholder, so the conversation shows what the model asked for rather than the file's bytes.
Knowledge Documents from a File
Upload with purpose=knowledge, then ingest the file into a knowledge base by id with POST /v1/knowledge-bases/{knowledgeBaseId}/documents/attach and {"fileId": "file_…"}. The request, its answers and the ingestion that follows are on the Knowledge page.
A knowledge file can only be uploaded by an organization member or an API key, never from an embedded panel. It cannot be deleted through /v1/files; delete the knowledge document instead.
Files from the Embedded Panel
The embeddable surface panel uploads what the user drops, pastes or picks with the paperclip at once, with purpose=agent_input and the panel's own signed assertion, and sends the resulting ids as input_file parts with the message. The panel refuses a file over 25 MB, and more than 10 files on one message, before anything is uploaded.
Files uploaded from a panel belong to the end user who uploaded them. Another end user of the same installation gets 404 for them, and the per-end-user quotas in Limits and Quotas apply. When the model hands such a file to a relay tool, the panel substitutes the bytes as described in Handing a file to a tool.
Permissions
| Caller | Upload | Retrieve, content, delete | List |
|---|---|---|---|
OWNER, ADMIN, MEMBER | Both purposes | Every file of the organization | Yes |
VIEWER | No | No | Yes |
USER (chat-only) | Both purposes | Only files they uploaded; others answer 404 | No |
| Embedded panel end user | agent_input only | Only files they uploaded; others answer 404 | No |
An API key acts with the organization role it was created with.
Errors
Errors on /v1/files use the OpenAI error envelope: { "error": { "message", "type", "param", "code" } }.
| Status | code | When |
|---|---|---|
400 | invalid_purpose | Unknown purpose on upload or list, or knowledge from an embedded panel |
404 | null (param: "id") | Unknown id, another organization's file, or another end user's file |
409 | file_in_use | DELETE on a knowledge file |
410 | file_expired | The file was deleted or passed its lifetime |
413 | file_too_large | The file is over 25 MB |
429 | file_quota_exceeded | An embedded panel end user is over the active-file count or the daily upload volume |
On /v1/responses, file references add three:
| Status | code | When |
|---|---|---|
400 | invalid_file | Malformed, unknown, foreign or out-of-scope file_id, a part with no source, or more than 100 distinct files |
400 | unsupported_file_source | file_data or file_url instead of file_id |
410 | file_expired | A referenced file was deleted or passed its lifetime |