# List generations

> Page through the generations on an account, filter it down, and download them.

Canonical page: https://www.rundiffusion.com/docs/api/library/list
Endpoint: GET /api/v2/library
Authorization: OAuth device flow or Personal API Access Token or Company API Access Token

---

The library is the generations on an account. List it to browse what is there, narrow
it with filters, and download individual items.

`GET /api/v2/library`

Authorization: OAuth device flow or Personal API Access Token or Company API Access Token

## Request

**Headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | string | Yes | Bearer token. This endpoint accepts an OAuth device flow token, a Personal API Access Token, or a Company API Access Token. See [Authentication](/docs/api/authentication). |

Every query parameter is optional. With none of them you get your own
generations on your personal account, newest first.

Which pool of generations you are reading follows `team_id`, the same way it
does on every endpoint: omit it for your personal account, name a team for that
team's. Here it also takes several team IDs separated by commas, to read across
teams in one page.

cURL:

```bash
curl "https://api2.rundiffusion.com/api/v2/library?limit=24" \
  -H "Authorization: Bearer $RUNDIFFUSION_TOKEN"
```

JavaScript:

```javascript
const page = await fetch(
  'https://api2.rundiffusion.com/api/v2/library?limit=24',
  { headers: { Authorization: `Bearer ${process.env.RUNDIFFUSION_TOKEN}` } },
).then(r => r.json());
```

Python:

```python
page = requests.get(
    "https://api2.rundiffusion.com/api/v2/library",
    headers={"Authorization": f"Bearer {os.environ['RUNDIFFUSION_TOKEN']}"},
    params={"limit": 24},
).json()
```

### Paging

**Query**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `cursor` | string | No | Opaque pagination token. When a response has has_more true, pass its next_cursor here to get the following page. Do not construct or parse one. |
| `limit` | integer | No | Page size, from 1 to 24. Default: `24` |

### Scope

The account selection names the pool; `scope` decides reach within it. `mine`,
the default, is your own generations. `all` is every member's, and it takes the
**Can manage everyone's generations and boards** (`MANAGE_ALL_GENERATIONS`)
permission.

Your own rows on a team, the default, need no permission at all:

```bash
curl "https://api2.rundiffusion.com/api/v2/library?team_id=Tq8vNc…" \
  -H "Authorization: Bearer $RUNDIFFUSION_TOKEN"
```

Whole-team reads take `scope=all`, and can span teams by naming several. The
permission is checked on **every** team named: a partly-authorized list is
refused with the failing teams in the error, never quietly narrowed to the ones
that passed, because a page from the narrowed list would be indistinguishable
from the full answer.

```bash
curl "https://api2.rundiffusion.com/api/v2/library?team_id=Tq8vNc…,Wm2xLp…&scope=all" \
  -H "Authorization: Bearer $RUNDIFFUSION_TOKEN"
```

**Query**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `team_id` | string | No | The team or teams to read. Several match any of them. Omit it to read your personal account. Part of the account selection rather than a filter, so it behaves identically on every endpoint. Accepts multiple values, comma-separated. |
| `scope` | string | No | Whose rows within the selected account. mine is the caller's own generations. all is every member's; it takes a team account and requires MANAGE_ALL_GENERATIONS on every team named in team_id. Default: `mine` One of: `mine`, `all` |
| `user_id` | string | No | Narrow scope=all to named members. Several match any of them. On the ordinary listing it selects whose generations; with sort=favorited it selects whose favorites. Under scope=mine you are already the answer, so sending it there is a 400 rather than being quietly overridden. Accepts multiple values, comma-separated. |

### Filters

**Query**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `board_id` | string | No | Narrow to a board. Applies to both the ordinary library and the favorites view. Several match any of them. Accepts multiple values, comma-separated. |
| `node_id` | string | No | Narrow to generations produced by a board node. Several match any of them. A board can hold the same tool more than once, so this is finer than board_id. Accepts multiple values, comma-separated. |
| `tool_id` | string | No | Narrow to generations produced by a tool. IDs come from [List tools](/docs/api/tools/list). Several match any of them. Accepts multiple values, comma-separated. |
| `aspect_ratio` | string | No | Aspect-ratio bucket, not an exact ratio. Anything that does not land in a listed bucket is OTHER. Several match any of them. Accepts multiple values, comma-separated. One of: `9:16`, `2:3`, `3:4`, `1:1`, `4:3`, `3:2`, `16:9`, `2:1`, `21:9`, `OTHER` |
| `resolution` | string | No | Resolution bucket measured on the long edge. Percent-encode the plus in 4K+ as 4K%2B, since a bare + means a space in a query string. Several match any of them. Accepts multiple values, comma-separated. One of: `1K`, `2K`, `4K+` |
| `model_family_tool_tag_id` | string | No | Narrow to a model-family tool tag. IDs come from [List tool tags](/docs/api/tool-tags/list). Several match any of them. Accepts multiple values, comma-separated. |
| `use_case_tool_tag_id` | string | No | Narrow to a use-case tool tag. IDs come from [List tool tags](/docs/api/tool-tags/list). Several match any of them. Accepts multiple values, comma-separated. |
| `media_tool_tag_id` | string | No | Narrow to a media tool tag. IDs come from [List tool tags](/docs/api/tool-tags/list). Several match any of them. Accepts multiple values, comma-separated. |
| `start_utc` | string (ISO 8601) | No | Start of the window, inclusive, in UTC. A generation created at exactly this instant is included. |
| `end_utc` | string (ISO 8601) | No | End of the window, exclusive, in UTC. A generation created at exactly this instant is NOT included, so a whole month is start_utc=2026-05-01T00:00:00Z with end_utc=2026-06-01T00:00:00Z. Must be after start_utc, or the request is rejected with 400. |

### Favorites

Favorites are two parameters because they are two independent questions.
`favorited` decides **which** generations come back. `sort` decides **what
order** they come back in.

**Query**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `favorited` | boolean | No | When true, return only generations the caller has favorited. It is an ordinary filter, so it stacks with every other one and works at either scope: pair it with scope=all to see your own stars inside a whole team library. Default: `false` |
| `sort` | string | No | Result order. created is newest-generated first, the same order as the rest of the library. favorited is most-recently-starred first, and requires favorited=true because ordering by when something was favorited is meaningless for things that are not. Default: `created` One of: `created`, `favorited` |

Pick `sort` by the question you are answering. "What did I star lately" is
`sort=favorited`. "Show my library, starred only" is the default `sort=created`,
which leaves the usual ordering alone.

> **What sort=favorited needs**
>
> `sort=favorited` orders by when something was starred, so it needs
> `favorited=true`. Without it there is nothing to order, and the request is a
> `400` naming the problem.
>
> Everything else stacks the way it does on the rest of the listing. `team_id`
> narrows to that team's generations, and `scope` decides whose favorites you
> are reading: `mine` is your own, `all` is every member's.

> **Reading another member's favorites**
>
> `scope=all` with `sort=favorited` reads other people's starred work, which is
> their behavior rather than their output. It takes the same
> **Can manage everyone's generations and boards** (`MANAGE_ALL_GENERATIONS`)
> permission as reading their generations, checked on **every** team in
> `team_id`, and a partly-authorized list is refused rather than narrowed.
>
> Add `user_id` to read one named member's. Without `scope=all`, you get your own
> favorites and nothing else.

> **Filters combine**
>
> Every filter narrows the same result set, so they stack. `scope=all` with
> `user_id`, `aspect_ratio=16:9`, and a `start_utc`/`end_utc` window answers "what
> widescreen generations did this teammate produce last month" in one request.
>
> Inside one filter, several comma-separated values widen it: `aspect_ratio=16:9,1:1`
> matches either bucket. Between filters they still narrow, so
> `aspect_ratio=16:9,1:1&tool_id=Tl9xBv…` means "widescreen or square, from that
> one tool". Each filter takes at most 25 values.

## Response

`200 OK` with one cursor page of generations.

```json
{
  "data": [
    {
      "id": "Kp7mZq…:3f2a9c14-…",
      "run_id": "Kp7mZq…",
      "result_id": "3f2a9c14-…",
      "type": "VID",
      "url": "https://rundiffusion.com/...",
      "thumb_url": "https://rundiffusion.com/...",
      "mime_type": "video/mp4",
      "width": 1080,
      "height": 1920,
      "duration_seconds": 15.1,
      "size_bytes": 7729000,
      "created": "2026-05-28T22:33:52Z",
      "tool_id": "1kCVin…"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJhIjoiMjAyNi0…",
  "last_cursor": null
}
```

### Top level

| Name | Type | Description |
| --- | --- | --- |
| `data` | array | The generations on this page, newest first. Empty when nothing matches. |
| `has_more` | boolean | Whether more pages exist beyond this one. This is the flag to branch on when paging. |
| `next_cursor` | string \| null | Pass this back as cursor to fetch the next page. Null on the last page. This is the one to page with. |
| `last_cursor` | string \| null | Echoes the cursor you sent on this request, so a response can be matched to what produced it. Null on the first page. It points backwards, so paging with it loops. |

### Generation object

| Name | Type | Description |
| --- | --- | --- |
| `id` | string | run_id and result_id joined by a colon. This is how a single item is addressed, and you can pass it back whole as a LIBRARY_REF input without splitting it. Example: Kp7mZq…:3f2a9c14-…. |
| `run_id` | string | The run that produced this item, as a 20-character opaque string. One run can produce several items. |
| `result_id` | string | This item within its run. |
| `type` | string | What kind of generation this is. The library serves renderable generations. One of: `IMG`, `VID`, `ASSET_3D`, `LAYERS` |
| `url` | string \| null | Signed download URL, valid for roughly seven days. Treat it as opaque and re-list to refresh it rather than storing it. |
| `thumb_url` | string \| null | Signed URL to a 512x512 WebP thumbnail, same seven-day expiry. Falls back to the same value as url in the short window after a run completes but before the thumbnail is generated, so there is always something displayable. |
| `mime_type` | string \| null | MIME type of the stored file, for example image/jpeg or video/mp4. |
| `width` | integer \| null | Pixel width. Null for formats without one. |
| `height` | integer \| null | Pixel height. Null for formats without one. |
| `duration_seconds` | number \| null | Runtime in seconds for video. Null for still generations. |
| `size_bytes` | integer \| null | Size of the file at url, in bytes. |
| `created` | string (ISO 8601) | When it was created, in UTC. |
| `tool_id` | string \| null | The tool that produced the item. Pass it back as tool_id to find everything else from the same tool. |

> **Page with next_cursor, not last_cursor**
>
> `last_cursor` echoes the cursor you sent, so paging with it re-requests the page
> you just read. Read `has_more`, and when it is true send `next_cursor` as
> `cursor`.

## Paging through everything

JavaScript:

```javascript
let cursor = null;
const items = [];

do {
  const url = new URL('https://api2.rundiffusion.com/api/v2/library');
  url.searchParams.set('limit', '24');
  if (cursor) url.searchParams.set('cursor', cursor);

  const page = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.RUNDIFFUSION_TOKEN}` },
  }).then(r => r.json());

  items.push(...page.data);
  cursor = page.has_more ? page.next_cursor : null;
} while (cursor);
```

Python:

```python
cursor, items = None, []

while True:
    params = {"limit": 24}
    if cursor:
        params["cursor"] = cursor

    page = requests.get(
        "https://api2.rundiffusion.com/api/v2/library",
        headers={"Authorization": f"Bearer {os.environ['RUNDIFFUSION_TOKEN']}"},
        params=params,
    ).json()

    items.extend(page["data"])
    if not page["has_more"]:
        break
    cursor = page["next_cursor"]
```

A page always holds as many items as you asked for until the library runs out.
Filtering happens before the page is assembled, so a page never comes back short
because of generations that were withheld or deleted.

## Identifying an item

`id` is the `run_id` and `result_id` joined by a colon, and that pair is how an
item is addressed anywhere else it comes up. Treat it as opaque and pass it
whole: the two halves are also returned separately, so there is never a reason
to split one yourself. [Get a generation](/docs/api/library/get) fetches one by
it, and [Delete a generation](/docs/api/library/delete) removes one.

## Errors

| Name | Type | Description |
| --- | --- | --- |
| `400` | VALIDATION_ERROR | A parameter failed validation. The common causes are a malformed team ID list in scope, an end_utc that is not after start_utc, a limit outside 1 to 24, and a value outside an enum. Sending a retired parameter lands here too, with the replacement named in the message. |
| `401` | UNAUTHENTICATED | Missing, malformed, or expired token. Refresh it and retry. |
| `403` | PERMISSION_DENIED | The caller cannot read the requested scope: scope=all without MANAGE_ALL_GENERATIONS on every team named in team_id. The error names the teams missing it. |
| `429` | RATE_LIMITED | Too many requests. Back off and retry per the Retry-After header. See [Rate limits](/docs/api/rate-limits#library). |

See [Errors](/docs/api/errors) for the full envelope and the code list.
