File attachments
Upload a file, then ask questions about it. The gateway extracts the file's text once, at upload, and folds that text into the prompt on every turn that names the file — so a spreadsheet or a deck can be discussed by any model, including ones with no vision or file support of their own.
Attachments are not a knowledge base. An attachment belongs to one conversation, expires, and is capped so it fits in a prompt; a knowledge base is a curated corpus that is chunked, embedded and retrieved. Upload a 400-page manual here and it will be truncated — send it to POST /v1/knowledge/{id}/files instead.
#Supported types
| Type | Extracted as |
|---|---|
.xlsx | One Markdown table per worksheet, in tab order. Dates are rendered as dates, not as Excel serial numbers. |
.csv, .tsv | A Markdown table. Quoted fields, embedded newlines and ;-delimited exports are handled. |
.pptx | Slide text in presentation order, one heading per slide. Speaker notes are not included. |
.docx | Paragraph text. |
.pdf | Text layer. A scanned or image-only PDF has none, and is rejected with no_text_extracted. |
.md, .markdown, .mdx | Prose, with link targets and formatting markers stripped. |
.txt, .json, .log | As-is. |
Legacy Office formats (.xls, .ppt, .doc) are not supported — re-save them in the current format. Images are not supported yet; only text is extracted.
#Upload a file
POST /v1/files takes one file as multipart/form-data.
curl -X POST http://localhost:5300/v1/files \
-H "Authorization: Bearer $RELAY_KEY" \
-H "X-Relay-User-Id: $USER_OBJECT_ID" \
-F "file=@Q3-sales.xlsx"
{
"id": "9f2c1b7e4a8d4e51b0c6d3f2a1e5c7b9",
"object": "file",
"filename": "Q3-sales.xlsx",
"bytes": 84213,
"characters": 11904,
"truncated": false,
"created_at": "2026-09-14T09:12:44Z",
"expires_at": "2026-09-15T09:12:44Z"
}
truncated: true means the file was larger than the prompt limits and only part of it was kept. The model is told this too, so it can say when an answer depends on the part it was not shown.
#Ask about it
Name the ids in file_ids on a chat completion or an agent run. It is a top-level member alongside messages; everything else about the request is unchanged.
curl -X POST http://localhost:5300/v1/chat/completions \
-H "Authorization: Bearer $RELAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Which region grew the most?"}],
"file_ids": ["9f2c1b7e4a8d4e51b0c6d3f2a1e5c7b9"]
}'
The same field works on POST /v1/agents/{id}/chat/completions, where the agent's own PII redaction setting is applied to the file's text as well.
Send file_ids on every turn the file is relevant to, not only the first. The gateway is stateless about attachments: it injects what the current request names. Re-sending an id costs nothing — the text was extracted once at upload and is not re-parsed.
#Who can read an attachment
An upload sent with X-Relay-User-Id belongs to that user, and only that user can name it in a later request. An upload sent without the header belongs to the whole workspace instead, which is the weaker position — send the header. Either way an attachment is invisible outside the workspace of the API key that created it.
An id that has expired, been deleted, or belongs to someone else is refused with 404 file_not_found, and the run does not proceed. This is deliberate: a request that quietly dropped an unreadable attachment would answer from the prompt alone, and the answer would look like the model had read the file.
#Managing attachments
| Endpoint | Purpose |
|---|---|
GET /v1/files/{id} | Metadata — size, character count, whether it was truncated, when it expires. |
GET /v1/files/{id}/content | The extracted text, exactly as a run would inject it. Useful when an answer looks wrong and you want to see what the model was actually given. |
DELETE /v1/files/{id} | Removes it and its text immediately. |
Attachments expire on their own after 24 hours by default.
#Limits
| Setting | Default | What it bounds |
|---|---|---|
Gateway:Files:MaxBytes | 25 MB | Upload size. |
Gateway:Files:MaxCharacters | 200,000 | Extracted text kept per file. |
Gateway:Files:MaxTabularRows | 200 | Rows kept per worksheet or CSV. |
Gateway:Files:MaxPerRequest | 10 | Attachments one request may name. |
Gateway:Files:TtlHours | 24 | Hours before an attachment expires. 0 disables expiry. |
The row and character caps exist because an attachment is pasted into a prompt, and a large export would exhaust the context window before the model answered anything. Knowledge-base ingest applies no such cap — that path chunks and embeds instead, so it keeps the whole file.
#In the panel
The Playground composer has a paperclip. Attached files appear as chips above the input and stay attached for the rest of the conversation, so follow-up questions still see them. A chip marked partial was truncated; remove one with its ×.