Files API

building

List and filter, or fetch one by id. Every result carries its URLs already.

Get files

Two calls, one job: reading files back out. Every result carries its url, variants[] and srcset inline, so a whole gallery is one request rather than one per file.

GET/v1/files

Confirmed files, newest first — or ordered by position when you filter by entity, so position 0 is the primary image and the array renders as it stands.

Request
1curl "$BASE/v1/files?entityType=product&entityId=prod_abc123&role=gallery" \2  -H "authorization: Bearer $KEY"

All parameters are optional and combine with AND.

ParameterMatches
entityTypeThe attached object's type
entityIdThe attached object's id
roleThe file's role on that object
ownerUserIdThe end user who uploaded it
visibilitypublic or private
variantVisibilitypublic or private
page1-based; 20 per page
Response
1{2  "files": [3    {4      "id": "E7iVnWA1LiEpu3u8dRTVK",5      "originalFilename": "sneaker front.jpg",6      "contentType": "image/jpeg",7      "size": 204800,8      "visibility": "public",9      "url": "https://cdn…/files/originals/public/…",10      "variants": [{ "width": 100, "url": "…" }],11      "srcset": "…100w, …300w, …",12      "entity": { "type": "product", "id": "prod_abc123", "role": "gallery", "position": 0 }13    }14  ],15  "total": 260,16  "page": 1,17  "totalPages": 1318}

Twenty per page, fixed. It is not a parameter, which is deliberate: a caller cannot ask for ten thousand rows and neither can a bug. A page past the end returns an empty array with the real total, not a 404.

Walking a large project

page counts rows to skip, which the database has to do one at a time — fine at page 2, expensive at page 500. For anything that walks a whole project, use the cursor instead:

Everything, a page at a time
1curl "$BASE/v1/files?cursor=$CURSOR" -H "authorization: Bearer $KEY"

Each response carries nextCursor, and its absence means you have reached the end — so "keep going while there is a cursor" is the whole loop:

Response
1{2  "files": [],3  "total": 259,4  "nextCursor": "MjAyNi0wOC0xN1QxMTowMDo0Ni4wMDBafGFiYzEyMw"5}

Treat it as opaque. It happens to be base64 today; anything you decode from it is not a promise.

Filtering by role alone is a scan across every entity in the project rather than a lookup — legitimate when you want every avatar, but pair it with entityType when you mean one kind of thing.

GET/v1/files/{id}

One file, when you kept the id yourself.

Response
1{2  "file": { "…": "the file object" },3  "url": "https://cdn…/files/originals/public/…",4  "variants": [{ "width": 100, "url": "…" }],5  "srcset": "…100w, …300w, …"6}

The URLs appear both inside file and at the top level; read them from file, the outer copies are the same strings kept for older callers.

Do not loop this over a listing. If you have the list, you have these URLs already — that is the N+1 the inline URLs exist to prevent.

Errors

404
1{ "error": "File not found" }

The same 404 covers a file that does not exist and one belonging to another project. Telling you which would confirm that someone else's id is real.