Plum / developers

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>"
}
FieldRules
filename1–255 characters after trimming. No slashes, backslashes, or control characters.
mimeA type/subtype value, stored in lowercase
size_bytesThe exact byte count, from 1 byte to 10 MiB
sha256The 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.txt

Storage 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.

ProblemResult
size_bytes above 10 MiB422 VALIDATION_ERROR on the intent
Missing or malformed sha256422 VALIDATION_ERROR on the intent
Bytes do not match the declared SHA-256Storage rejects the upload with 400
Changed headers or an expired signed requestStorage rejects the upload with 403
An object is already stored for this fileStorage rejects the upload with 412
Confirmation before a complete, matching upload409 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

OperationRequest
List filesGET /files returns { items, cursor }, newest first
Read a descriptorGET /files/{id}
RenamePATCH /files/{id} with { "filename": "new-name.txt" }
DeleteDELETE /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.

On this page