Private files
Upload, confirm, read, and delete small account-owned files.
Files are private account storage for small attachments of up to 10 MiB. Uploading a file does not create audio, a recording stream, or a transcript. To transcribe audio, follow Upload audio instead.
Every /files request uses an account bearer session and X-API-Version: 1.
Upload a file
A file is uploaded in three steps: declare it, send its bytes directly to storage, then confirm it.
1. Create an upload intent
Call POST /files/intent:
{
"filename": "notes.txt",
"mime": "text/plain",
"size_bytes": 1234,
"sha256": "<64 lowercase hex characters>"
}| Field | Rules |
|---|---|
filename | 1–255 characters after trimming. No slashes, backslashes, or control characters. |
mime | A type/subtype value, stored in lowercase |
size_bytes | The exact byte count, from 1 byte to 10 MiB |
sha256 | The SHA-256 of the exact bytes, as 64 lowercase hex characters |
The 201 response contains the new file descriptor, with status pending_upload, and a signed upload request:
{
"success": true,
"data": {
"file": { "id": "<file_id>", "status": "pending_upload", "url": null, "...": "..." },
"upload": {
"url": "https://<storage host>/<bucket>/<key>?X-Amz-Signature=...",
"method": "PUT",
"headers": {
"Content-Type": "text/plain",
"If-None-Match": "*",
"x-amz-checksum-sha256": "<base64 digest>"
},
"expires_at": "2026-09-29T00:30:00.000Z"
}
}
}2. Upload the bytes
Send the raw bytes to upload.url with upload.method and exactly the headers in upload.headers. The URL is the credential: do not add your bearer token or X-API-Version, and do not log it. Do not use multipart form data.
curl -X PUT "$UPLOAD_URL" \
-H 'Content-Type: text/plain' \
-H 'If-None-Match: *' \
-H "x-amz-checksum-sha256: $CHECKSUM" \
--data-binary @notes.txtStorage rejects bytes that do not match the declared SHA-256 and never replaces an uploaded object. The signed request expires at upload.expires_at (30 minutes); create a new intent after that.
3. Confirm the file
Call POST /files/confirm:
{ "file_id": "<file_id>" }Optima checks the stored object's size, MIME type, and SHA-256 against the intent and returns the descriptor with status ready. Keep the local original until confirmation succeeds.
See create an intent and confirm a file for full schemas.
Retries and errors
Stored file bytes are immutable. If an upload response is lost, call confirm. If confirmation returns 409 because the bytes were not stored, repeat the signed upload (or create a new intent once it expires) and confirm again. Confirmation can be repeated safely. To change a file's contents, create a new intent.
| Problem | Result |
|---|---|
size_bytes above 10 MiB | 422 VALIDATION_ERROR on the intent |
Missing or malformed sha256 | 422 VALIDATION_ERROR on the intent |
| Bytes do not match the declared SHA-256 | Storage rejects the upload with 400 |
| Changed headers or an expired signed request | Storage rejects the upload with 403 |
| An object is already stored for this file | Storage rejects the upload with 412 |
| Confirmation before a complete, matching upload | 409 CONFLICT |
The MIME type is declared by the caller. Optima checks that the upload matches the declaration; it does not inspect the file's format.
Download a file
A ready descriptor includes url, a signed link that downloads the bytes directly from storage until url_expires_at (15 minutes). GET it without API headers; the response carries the declared MIME type. Read the descriptor again for a fresh link. Pending files have a null url.
Read, rename, and delete
| Operation | Request |
|---|---|
| List files | GET /files returns { items, cursor }, newest first |
| Read a descriptor | GET /files/{id} |
| Rename | PATCH /files/{id} with { "filename": "new-name.txt" } |
| Delete | DELETE /files/{id} returns 204 |
A descriptor contains id, filename, mime, size_bytes, sha256, status, url, url_expires_at, created_at, and updated_at. Lists include pending files. Pagination follows the standard pattern.
Deleting removes the bytes and the descriptor. Repeating a delete, or deleting an unknown ID, also returns 204.
When a workspace is deleted, its files' bytes are released: they are left out of GET /files, and reading, renaming, or confirming one returns 410 CONTENT_DELETED with removed_at. See Removed content.
See list files, get a file, rename a file, and delete a file.