Skip to main content
A message on /v1/chat/completions can carry a file. Put it in the message’s content as an array of parts, the same shape an OpenAI client already sends:
An image uses image_url instead:
A message whose content is a plain string keeps working exactly as before. Nothing changes for a request that attaches nothing.

What you can attach

Any media type. The /v1 endpoint does not restrict the format: the data URL’s media type is passed to the provider as you wrote it, and what a model makes of the file is between you and the model you picked. Only two things are checked, and both are about the request rather than the format:
  • It must be a data: URL. A remote https:// address is refused rather than fetched, because fetching an address a caller chose would have the platform make a request from inside its own network, past the egress guard.
  • It must fit the limits below: 14 MB for one file, 7 MB for one image, and 14 MB for everything in the request together.
Only PDFs and images are known to be read by a model, which is what the capabilities below record. Anything else reaches the provider untouched and untested, so try a format with the model you route to before relying on it. Nothing is stored. The bytes are in your request body, go to the provider, and are gone when the response returns.

Akumi Chat is stricter, and deliberately

Akumi Chat uploads a file first, to POST /chat/v1/attachments, and names the id it returns in the run’s attachments array. That path keeps the file for 90 days and serves it back to everyone in the conversation, so it restricts what it accepts where /v1 does not: Ten files per message, and the same size limits as /v1. GET /chat/v1/attachments returns the live list and the live numbers, so a client builds its picker from them rather than holding a copy. There is no switch for attaching files, and there used to be: it existed only because nothing looked inside one, so the only honest question was whether a file could be sent at all. A document is now read and its text goes through the firewall like anything typed, and a picture is refused or questioned where a workspace blocks or asks about a kind of data, so the kinds table answers what that switch used to. Files carrying macros are refused there, whatever they are called, because a macro-enabled workbook is indistinguishable from an ordinary one by its type alone. That is not virus scanning and does not pretend to be: it refuses one known carrier, and it exists because a file uploaded by one person is opened later on another person’s machine. The legacy .xls and .doc are refused for the same reason, and Apple’s .pages, .numbers and .key because they are archives with no signature of their own. Exporting any of those to PDF, .docx or .xlsx works. Google needs nothing: a Google Doc or Sheet has no file until you export it, and it exports as a format already listed. Outlook’s .msg is refused like .doc, for the same reason; .eml is the portable form and is accepted. Photographs and scans need converting first. A phone photograph of a signed page is HEIC and a scanner writes TIFF, and no model this platform routes to reads either: they take PNG, JPEG, GIF and WebP. Both are refused at upload rather than accepted and answered wrongly, so the person still has the original open when they find out.

Pick a model that can read it

Two capabilities on GET /models say what a model does with an attachment: Every model in the catalogue was sent a real file and asked to quote a code printed inside it, so these are measured rather than copied from a provider’s documentation. Build your picker from GET /models and grey out the models that cannot read what your user attached. A model that cannot read the file is dropped from the chain, not sent to. If you name a single model, the request comes back 422 no_routable_model. If you send a models chain, the entries that cannot read the attachment are skipped and the first one that can serves the request. This is deliberate: a model handed a PDF it cannot open answers from the text beside it, and nothing in that answer tells you the file went unread.

Limits

  • 14 MB per file, measured on the decoded bytes rather than the base64 string.
  • 7 MB per image, which is lower on purpose. See below.
  • 14 MB per request, counting every attachment in it.
  • 20 content parts per message.
  • Inline data only. data:<media type>;base64,<bytes> is accepted; a remote https:// URL is refused. The platform will not fetch a file from an address you name, so fetch it yourself and send the bytes.
A file over any of these is refused with a 422 naming the field, before the request reaches a provider.

Why an image is capped below a document

Because bytes are not what a file costs. A model reads an image as tiles and a document as tokens, and the providers behind this platform publish limits that reflect that: Anthropic accepts one image up to 10 MB once it is base64-encoded, which is 7 MB of actual bytes, while a document is bounded only by the request carrying it. The request number comes from the same place. Base64 is a third larger than what it carries, and the smallest request any of these providers accepts is 20 MB, which leaves 14 MB of files and room for the conversation around them. These are the smallest ceilings, not the largest. A file accepted here and refused by a provider is the worst way this can fail: you have already waited, and the error names a limit you did not set. So the platform refuses at the edge, on the number that holds everywhere it might route.

A tool can return a file

Akumi Chat only. When a run stops on a tool call, the answer may carry files as well as text:
The same ids a message takes, refused the same way: an id that does not resolve, one from another workspace, one already on a message, one past its window. Up to ten, and the same total size a message may carry. Where it lands in the prompt is not where it is stored. The file is kept on the tool result, because that is what happened, and the conversation shows it there. In the prompt it rides on a short message immediately after, because that is the only position every provider reads: measured on 2026-09-14, a picture attached to a tool result reached Mistral’s models and reached neither OpenAI model, with no error on either call. The model still needs to be able to read it. A turn carrying a picture routes only to a model with vision, and a document only to one that takes documents, exactly as a message does.

Residency

An attachment travels with the request and goes wherever the request goes. Routing rules apply to it unchanged: send "provider": {"residency": "eu"} and the file only reaches an EU-resident model, the same as the text beside it.

What the firewall does, and where

On /v1, it does not look inside an attachment. It masks text, and a file on this API is bytes in a request that the platform passes on. A file reaches the model exactly as you sent it, whatever your firewall setting says. Decide before you attach, not after. In Akumi Chat, a document is read into text when it is uploaded, and that text goes through the firewall like anything somebody types. A spreadsheet, a presentation, a PDF, a scanned page and a picture printed on a page are all read. The words are masked with the message they arrived on, under one set of placeholders, so the same name in both becomes the same placeholder and comes back as the same person. The difference is the upload. /v1 has no upload step: the bytes arrive on the inference call itself, and there is nowhere to read them that is not the hot path of your own request. An image is not read into text in either place. A model given a picture needs the pixels, and pixels cannot be masked. In chat an image is read for the kinds a workspace blocks, and refused when one is found; a workspace that blocks nothing reads no images at all. Reading a photograph is less reliable than reading a page, so a number on a blurred or angled picture can be missed and a block on it may not fire. The platform records what happened rather than leaving you to infer it. Every request carrying a file writes an attachments entry on its trace holding how many files there were, of what media types, how large, and whether anything read them. A chat message says which kinds were masked inside each of its files. What the files held is not recorded anywhere. In Akumi Chat the upload itself answers for the file you just sent. POST /chat/v1/attachments returns read, true when the words were read and will go to the model as text, false when the file travels as bytes: an image, a format the reader does not know, a damaged file, a scan over the page budget. GET /chat/v1/attachments answers the other question, whether the platform reads files at all, which it now does.

Reading a file back

GET /chat/v1/attachments/ Answers with the bytes and the stored media type. Governed by membership of the conversation the file was sent into, which is what governs everything else about that conversation. 404 covers both “wrong id” and “not yours” on purpose, so the endpoint cannot be used to learn that a file exists somewhere else. 410 is the one refusal that says otherwise, because “gone” and “never existed” are different things to show somebody. The response carries an ETag and Cache-Control: private, max-age=86400, immutable. An attachment is written once and never overwritten, so it can be cached hard; private because the response belongs to one person. A file that has been uploaded and not yet sent belongs to whoever uploaded it and to nobody else. There is no conversation to be a member of yet.

Where the bytes live

On this API, nowhere. An attachment lives for the length of the request: it arrives in the body, it goes to the provider, and nothing writes it down. There is no attachment store to list, expire or delete. In Akumi Chat, for 90 days. A conversation re-sends its files on every later turn, so they have to be stored. They are kept for 90 days from upload and then deleted, and the text read out of a document goes in the same write: it is the same document with some values replaced, so it cannot outlive the file it came from. The window is deliberately not the life of the conversation. Nothing expires a chat thread: it lives until somebody deletes it, and most never will, so “kept with the thread” would mean kept forever. After the 90 days the message still says a file was attached, what it was called and when it expired. The conversation carries on; the model can no longer be shown that file, and it no longer has the text either. A message in the transcript carries what it was sent with, so a conversation read back a week later still shows its files:
The key is absent on a message that carried nothing. expired is the platform’s answer rather than a date comparison, and it stays true after the bytes go while the rest of the entry keeps saying what was there. Deleting a conversation deletes its files with it, without waiting for the window. The provider you routed to has its own retention, which is its own to state. Routing to an EU-resident model keeps the file in the EU.