<!-- section: Files API · status: building · source: docs/files-api/03-get-files.md -->

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

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

All parameters are optional and combine with AND.

| Parameter | Matches |
|---|---|
| `entityType` | The attached object's type |
| `entityId` | The attached object's id |
| `role` | The file's role on that object |
| `ownerUserId` | The end user who uploaded it |
| `visibility` | `public` or `private` |
| `variantVisibility` | `public` or `private` |
| `page` | 1-based; 20 per page |

```json Response
{
  "files": [
    {
      "id": "E7iVnWA1LiEpu3u8dRTVK",
      "originalFilename": "sneaker front.jpg",
      "contentType": "image/jpeg",
      "size": 204800,
      "visibility": "public",
      "url": "https://cdn…/files/originals/public/…",
      "variants": [{ "width": 100, "url": "…" }],
      "srcset": "…100w, …300w, …",
      "entity": { "type": "product", "id": "prod_abc123", "role": "gallery", "position": 0 }
    }
  ],
  "total": 260,
  "page": 1,
  "totalPages": 13
}
```

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:

```bash Everything, a page at a time
curl "$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:

```json Response
{
  "files": [ … ],
  "total": 259,
  "nextCursor": "MjAyNi0wOC0xN1QxMTowMDo0Ni4wMDBafGFiYzEyMw"
}
```

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.

```json Response
{
  "file": { "…": "the file object" },
  "url": "https://cdn…/files/originals/public/…",
  "variants": [{ "width": 100, "url": "…" }],
  "srcset": "…100w, …300w, …"
}
```

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

```json 404
{ "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.
