Uploading files
Bytes never pass through the API. An upload is three calls, and the middle one goes straight to storage:
Code
The file record exists from step 1, but a file that is never finalized never appears: it stays invisible to every list, and nothing processes it. Step 3 is required rather than bookkeeping.
Step 1: request upload URLs
Batched, up to 300 files per call:
| Field | |
|---|---|
file_name | Required. The extension decides the stored object's extension. |
file_type | Required. The MIME type. The signed URL is pinned to it, so the PUT must send the same one. |
file_size | Required, in bytes. Used for the quota check and to bound the signed URL. |
purpose | Required. Which library the file belongs to. See below. |
file_repo | Optional. Which repository the file belongs to, and how a document joins a knowledge base. See below. |
album_ids | Optional. Pre-assigns the file to albums, so it lands already filed. |
mediaTags | Optional. Media tags applied on arrival. |
Repository
file_repo is one key naming what the file belongs to. You must be able to write to
whatever it names: filing a document into a knowledge base you cannot edit is refused
rather than silently accepted.
| Key | Value | |
|---|---|---|
library | ["media"] or ["docs"] | The shared media library, or the document library. An array, because a library is a place rather than an object; generated is the third name in it, and the platform sets that one. |
ds | knowledge base id | The document joins that knowledge base and becomes retrievable. |
thread | conversation id | The file belongs to that conversation. |
design | design id | The file belongs to that design. |
prompt | prompt id | A reference attached to that saved prompt. |
More than one key may apply. An image generated inside a conversation carries both the
generated library and its thread, and is returned by either listing.
Omit file_repo and the file belongs to nothing in particular, which is correct for a
font or a profile picture. Such a file is fetched by id and never appears in a listing,
because every listing names a repository.
A file read back may carry other keys: the presentation job that generated it, the form
submission it arrived on, the workflow run behind it. These record how the file came to
exist, are set only by the platform, and are rejected if you send them. Drop them before
echoing a file's own file_repo back on a copy.
Move a file afterward with POST /files/{id}/move rather than a field on PUT /files/{id}: where a file sits decides who can reach it, so both ends are checked.
Purpose
purpose decides where the file lives and which types are accepted. The three most common
values:
| Purpose | For | Accepts |
|---|---|---|
media | The media library, the DAM proper | Images, video, audio |
document | The document library | Documents: PDF, office formats, text |
file-repo | General file storage | Any type |
The rest cover narrower cases: knowledge (files backing a knowledge base),
media-knowledge (reference images for AI, images only), font and chat. Those seven
are the complete list, and anything else is rejected with a 422.
Each purpose is gated by its own permission, so a key that may upload media is not thereby allowed to upload knowledge.
The response mixes accepted and rejected files
The response is an array in the order you sent, but it contains both accepted and
rejected files. An accepted entry is the created file record plus an uploadUrl, and a
rejected one carries rejection_reason and no URL:
Code
The call returns 200 either way. A rejection here concerns that file (its type does not
suit its purpose, it exceeds the per-file size limit, or your key lacks the permission that
purpose requires) rather than the request being wrong. A malformed request, including a purpose
outside the seven above, returns 422 for the whole call.
Branch on the presence of uploadUrl rather than on the status code:
One condition does fail the whole batch: exceeding your storage entitlement returns 403 before any URL is issued, because the check runs against the batch total.
Step 2: PUT the bytes
Send them straight to the signed URL with no API credentials, because the URL is the authorization:
Three constraints are encoded in the URL itself, so getting them wrong fails at storage rather than at the API:
- It expires after one hour. For a long queue, request URLs in batches as you go rather than all at once up front.
- The
Content-Typemust match thefile_typeyou declared. - The size is bounded to your declared
file_sizeplus 1%, capped at the per-file maximum.
If a URL expires before you use it, PUT /files/{id}/update-url issues a fresh one for
the same file, so you do not lose the record or its album assignments.
This is also why the API's 10 MB request body cap does not apply to uploads: it governs JSON bodies sent to the API, and your bytes never go there.
Step 3: finalize
Finalize confirms the bytes arrived, records their true size, type and MD5, and hands the file to processing: thumbnails, AI analysis, and indexing. It answers with the file record.
Finalize each file with its own PUT as soon as its bytes land, rather than finalizing
the batch at the end. Processing starts sooner and one slow upload no longer holds up the
rest.
Check when a file is ready
Processing is asynchronous. Poll GET /files/{id} and watch file_status:
file_status | Means |
|---|---|
pendingUpload | The row exists, the bytes have not been confirmed. Pre-finalize. |
pending | Finalized, queued for or undergoing processing. |
ready | Processing finished. Thumbnails, analysis and embeddings are in place. |
error | Processing failed. The file stays, but its renditions do not. |
Poll on the order of seconds rather than milliseconds. A small image is usually ready in a few seconds, while video and long documents take proportionally longer, and every poll spends rate-limit quota.
Most integrations do not need to wait at all. The file is listed and downloadable as soon as it is finalized. Waiting matters only when your next step depends on something processing produces, such as a thumbnail, extracted text, or the embeddings that similarity search needs.
Import from Dropbox or Google Drive
Importing skips the local round trip: you supply links, and the platform fetches them. Between 1 and 500 files per call.
Code
provider is dropbox or google-drive. Dropbox entries carry a direct link, while
Drive entries carry driveFileId and an accessToken instead. Poll GET /file-imports/{id} for progress. Imported files move through cloudImport and
importing before reaching the same ready state as any other file.
Versions
Uploading a corrected file over an existing one keeps the history rather than replacing it. Upload the new file normally, then attach it as a version of the original:
Code
GET /files/{id}/versions lists them, POST /files/{id}/versions/{version_id}/revert
makes an older version current again, and each version has its own download URL.
Reverting is a swap rather than a delete: the version you were on becomes a version in
the history.
Versions count against your storage entitlement, because the bytes are retained.
Delete and recover files
DELETE /files/{id} is a soft delete: the file leaves the library but the bytes remain
for a grace period. GET /files/trash lists what is recoverable, and POST /files/trash/{id}/recover restores one.
What to check when an upload fails
| Symptom | Usual cause |
|---|---|
403 on upload-urls | Storage entitlement exceeded for the batch total. |
rejection_reason on an entry | The type is not allowed for that purpose, or the file exceeds the per-file size limit. |
The PUT to storage fails | Content-Type does not match file_type, the body is larger than declared, or the URL expired. |
| Finalize returns an error | The bytes never landed, meaning the PUT did not succeed. |
Stuck at pending | Processing is queued or slow. A permanent failure ends at error rather than pending. |
| File never appears in a list | It was never finalized. |
Next steps
- Organizing and finding assets covers albums, tags, and the filter vocabulary.
- AI enrichment covers what analysis produces, and searching by meaning.
- Delivering assets covers download URLs, bulk ZIPs and share links.
