attachment_ids on the turn that asks about it. The platform stores the bytes and hands the files to the model on that turn. Apart from opening zips and emails into their files, it does not extract text or keep a parsed copy; the model reads the file itself.
In a deployment with no file storage configured, attachment routes answer 503 attachments_unavailable. The policy read (GET .../attachments/policy) still answers, because it only reads the instance config.
How it works
- The instance’s latest published config enables uploads in its
attachmentsblock. The draft never governs uploads. No published version is409 instance_not_published; uploads off is400 attachments_not_enabled. - You upload a file with one of three paths: a multipart upload, a reserve then PUT then finalize upload for large files, or a folder.
- The platform checks the type, size, and contents, and stores a record with status
ready. - You send a turn with
messageandattachment_ids. The stream starts with anattachmentframe confirming each file and how it was read.
Read the policy first
GET .../conversations/{conversation_id}/attachments/policy?end_user=... returns the limits the instance enforces, so your interface can refuse a file before uploading it.
200:
max_bytes_per_message is measured on the base64-encoded size the model provider receives, which is about four thirds of the raw size. A disabled instance answers 400 attachments_not_enabled rather than enabled: false.
Accepted formats
The platform accepts a closed set of formats. A file is classified by its extension (the declared content type is used only for a name with no extension), and its bytes must agree with that type.
When a read limit applies, the file’s record carries a
notice in plain words. For a file sent on its own, the attachment frame carries it too; for a file inside a container, read the member’s record. Show it to the user: an answer drawn from part of a schedule is not an answer about the whole schedule.
Outlook .msg files are refused with a message that says to save the email as .eml or attach the file inside it directly.
How a zip or email is opened
A.zip or .eml is accepted only when the instance sets unwrap_archives: true. It is opened when it is stored, by a multipart upload or at finalize, and the same rules apply on both paths:
- It is opened one level deep, up to 100 MiB decompressed; a zip may list at most 10,000 entries.
- Files are extracted by name. An entry with an unusable name, an empty file, a nested zip or email, or a name of a type the platform never accepts is skipped. Extraction collects at most 200 files; files past the 200th are omitted. An email’s
From,To,Cc,Date, andSubjectheaders and its body text are kept as one more file,message.txt. - Each extracted file is then checked by the rules for a file sent on its own. A file over
max_bytes_per_file, of a type the instance does not accept, or whose bytes disagree with its name is skipped. The 200 extracted files count toward that ceiling whether or not they pass this check, so files that fail it can use up the allowance before a later acceptable file. - At most 25 accepted files are kept. Accepted files past the 25th are omitted, not refused, so the upload still succeeds.
- Each kept file is stored as its own attachment, pointing back at the container. When anything was omitted or skipped, the container’s
noticesays how many files were read, names up to 20 omitted files and counts the rest, and counts the skipped ones. Show it to the user.
A folder differs: it is refused above 25 files (see Upload a folder).
Upload a file (multipart)
POST .../attachments with multipart/form-data fields end_user and file. The body may be up to max_bytes_per_file plus 1 MiB. Use this path when your server holds the bytes.
201:
ready record, as long as the current policy still accepts it, and does not count against the conversation limit.
Upload a large file (reserve, PUT, finalize)
Use this path to send bytes straight from the user’s browser to storage, for example when your own server caps request bodies. Your server calls reserve and finalize with its key; the browser only receives a one-file upload URL.- Reserve.
POST .../attachments/reservewith{end_user, filename, content_type, size_bytes}. The platform checks the name and declared size, creates a record with statusstoring, and returnsupload_url, a presigned PUT URL for this one file that expires in 15 minutes. - PUT the bytes to
upload_urlwith noAuthorizationheader. The URL can only write this one file. - Finalize.
POST .../attachments/{attachment_id}/finalizewith{end_user}. The platform reads what arrived, measures the real size, checks the type against the bytes, and marks the recordready, or refuses it.
201:
200 with {attachment} at status ready, with its notice when the format has a read limit. Finalize is safe to repeat: a second call on a ready record returns it again. When a file that is not in a folder finalizes with the same bytes and name as a ready file the conversation already holds, finalize returns the existing record (with its own id) and retires the reservation, so always use the id from the finalize response. The daily cleanup pass removes a reservation nobody finalized within an hour.
Upload a folder
A folder is a container your application opens: you send its list of files, and the platform returns one upload URL per file. The folder counts as one attachment towardmax_files_per_message.
POST .../attachments/folderswith{end_user, name, files: [{path, size_bytes}]}. At most 25 files per folder.- The response has
container(the folder record),members(one reservation per file, each withupload_urlandsource_pathechoing thepathyou sent), andskipped(files the instance will not read, each withpathandreason). The rest of the folder still opens. A folder with nothing readable is refused. - PUT each member’s bytes to its
upload_url, then finalize each member by itsattachment.id. - When every member is finalized, the folder is
ready. Name the container id inattachment_ids. A turn reads a folder’s files only when the instance setsunwrap_archives: true; otherwise the upload succeeds but the turn answers400 attachment_not_accepted.
ready in the conversation, the response is 200 with reused: true, no members, and nothing to upload. Otherwise a new folder opens with 201. A file whose content changed but whose path and size did not is not detected, so delete the old folder before you upload the changed one. The folder and its files together count toward the conversation’s 100-attachment limit.
notes.msg is listed in skipped with its reason, and the two PDFs open.
Send files with a message
Name ready attachments inattachment_ids alongside message. A container (zip, email, or folder) resolves to the files inside it. In this example the user compares two supplier contracts.
attachment frame before the conversation frame:
document_reference block, never the bytes. A file sent on its own gets one block with its id, name, media type, and notice. Files that came out of a container are recorded in a members array inside a block that carries the container’s id and name.
Later turns carry earlier attachments forward on their own; you do not name them again. Each turn gives the model the conversation’s attachments, newest first, as long as each file still passes the instance’s current attachment policy and fits the per-turn byte budget (50 MiB once base64-encoded). A file the current policy or the budget leaves out, or one deleted since, stays in the transcript and is named to the model as out of view. Files ride on the messages that carried them, so a file on a message older than the history window (max_history_messages) is not sent.
A turn is refused, before anything streams, when the files cannot all be used. It never runs with fewer documents than you named.
If a named attachment is deleted after these checks and before the first model call, the stream ends with an
error frame whose code is attachment_changed.
Limits
Attachment record and statuses
When finalize finds that the bytes never arrived, it answers
400 attachment_rejected and leaves the record storing: PUT the bytes (while the upload URL is valid) and finalize again, or leave it and the cleanup pass removes it once it is more than an hour old. Any other refusal at finalize removes the record at once, and a later read of it answers 404 attachment_not_found. The finalize error response carries the reason.
Read, list, download, delete
GET .../attachments?end_user=...&offset=...lists up to 100 records per page, newest first, including files inside containers. Readhas_moreand passnext_offsetback asoffset. A bad offset is400 invalid_offset.GET .../attachments/{attachment_id}?end_user=...returns{attachment}.GET .../attachments/{attachment_id}/content?end_user=...answers302to a URL that expires in 5 minutes and downloads the file asapplication/octet-stream. Only areadyrecord has content; any other answers404 attachment_not_found.DELETE .../attachments/{attachment_id}?end_user=...answers204. The record disappears at once. The daily cleanup pass removes the stored bytes once an hour has passed. Deleting a container deletes its files. A file inside a zip or email cannot be deleted alone (409 attachment_member_delete); delete the container. A file inside a folder can be deleted alone.
end_user; a miss on any of them is 404.
Retention
attachments.retention_days sets how long files live; null follows the conversation’s retention. When a conversation is deleted by retention, its attachments are swept after it. See Export and retention.
Upload errors
Turns and streaming
The
attachment frame and turn errors.Upload an attachment (API reference)
The generated endpoint contract.