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:

purposeForLifetimeDeletable here
agent_inputAttaching to agent turnsExpires 24 hours after it was last used in a turnYes
agent_outputFiles an agent's shell wrote to outputs/; created by the server onlyKept for 30 days from creation; a turn that names it does not extend thatYes, by the person who asked for it and by organization admins
knowledgeIngesting into a knowledge baseNever expires; belongs to the knowledge documentNo, 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.

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}/content answers 409 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 answer 404 to everyone else, and /preview answers 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_file from another conversation: the request is refused with 409 file_held before 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 parameterMeaning
purposeagent_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
pageZero-based page number. Default 0
sizePage 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

LimitValueApplies toError
File size25 MBEvery upload413 file_too_large
Active files50 agent_input files not yet deletedEach end user of an embedded panel429 file_quota_exceeded
Upload volume200 MB in any rolling 24 hoursEach end user of an embedded panel429 file_quota_exceeded
File references100 distinct file_ids per /v1/responses requestEvery turn400 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:

ToolReceives
A relay tool of an embedded panelThe 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 clientThe 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

CallerUploadRetrieve, content, deleteList
OWNER, ADMIN, MEMBERBoth purposesEvery file of the organizationYes
VIEWERNoNoYes
USER (chat-only)Both purposesOnly files they uploaded; others answer 404No
Embedded panel end useragent_input onlyOnly files they uploaded; others answer 404No

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" } }.

StatuscodeWhen
400invalid_purposeUnknown purpose on upload or list, or knowledge from an embedded panel
404null (param: "id")Unknown id, another organization's file, or another end user's file
409file_in_useDELETE on a knowledge file
410file_expiredThe file was deleted or passed its lifetime
413file_too_largeThe file is over 25 MB
429file_quota_exceededAn embedded panel end user is over the active-file count or the daily upload volume

On /v1/responses, file references add three:

StatuscodeWhen
400invalid_fileMalformed, unknown, foreign or out-of-scope file_id, a part with no source, or more than 100 distinct files
400unsupported_file_sourcefile_data or file_url instead of file_id
410file_expiredA referenced file was deleted or passed its lifetime

Was this page helpful?