# Fetching Data from the Playbook API

Retrieve key resources — organizations, boards, and assets—using simple GET calls.

## Prerequisites[​](#prerequisites "Direct link to Prerequisites")

1. **Access token**: a valid Playbook API token with read permissions, sent in the `Authorization: Bearer` header.
2. **Organization Slug** (`slug`): Your organization's unique identifier.
3. **Board Token** (`board_token`): Token of a specific board (for assets endpoint).

***

## List Your Organizations[​](#list-your-organizations "Direct link to List Your Organizations")

### Endpoint[​](#endpoint "Direct link to Endpoint")

```
GET /organizations
```

### Authentication[​](#authentication "Direct link to Authentication")

Send `Authorization: Bearer YOUR_TOKEN` with the request.

### Query Parameters[​](#query-parameters "Direct link to Query Parameters")

* `page` (integer, optional, default: 1)
* `per_page` (integer, optional, default: 20)

### Example Request[​](#example-request "Direct link to Example Request")

```
curl "https://api.playbook.com/v1/organizations?page=1&per_page=20" \

  -H "Authorization: Bearer YOUR_TOKEN" | jq
```

### Example Response[​](#example-response "Direct link to Example Response")

```
{

  "data": [

    { "id": 1, "slug": "coolclient-ltd", "name": "CoolClient Ltd" },

    { "id": 2, "slug": "acme-corp", "name": "Acme Corp" }

  ],

  "pagy": { "current_page": 1, "page_items": 20, "total_pages": 1, "total_count": 2 }

}
```

***

## List Boards in an Organization[​](#list-boards-in-an-organization "Direct link to List Boards in an Organization")

### Endpoint[​](#endpoint-1 "Direct link to Endpoint")

```
GET /{slug}/boards
```

### Authentication[​](#authentication-1 "Direct link to Authentication")

Send `Authorization: Bearer YOUR_TOKEN` with the request.

### Query Parameters[​](#query-parameters-1 "Direct link to Query Parameters")

* `page` (integer, optional)
* `per_page` (integer, optional)
* `depth` (integer or `all`, optional, default `1`) — `1` returns only top-level boards; a number returns boards down to that level; `all` returns every board in the workspace
* `query` (string, optional) — filter by board title
* `displayable_assets` (boolean, optional, default `true`) — each board carries up to three preview assets in `displayable_assets`. Building them is most of what this request costs, so pass `false` if you do not show previews; the field then comes back `null`. `GET /{slug}/boards/{board_token}`, `/children` and `/descendants` accept it too.

### Example Request[​](#example-request-1 "Direct link to Example Request")

```
curl "https://api.playbook.com/v1/coolclient-ltd/boards?page=1&per_page=20" \

  -H "Authorization: Bearer YOUR_TOKEN" | jq
```

### Example Response[​](#example-response-1 "Direct link to Example Response")

```
{

  "data": [

    { "id": 123, "token": "boardToken111", "title": "Homepage Assets" },

    { "id": 124, "token": "boardToken222", "title": "Blog Banners" }

  ],

  "pagy": { "current_page": 1, "page_items": 2, "total_pages": 1, "total_count": 2 }

}
```

***

## List Every Board Under a Board[​](#list-every-board-under-a-board "Direct link to List Every Board Under a Board")

To get a board's whole subtree — children, their children, and so on — in one paginated call instead of walking it level by level:

```
GET /{slug}/boards/{board_token}/descendants
```

The board itself is not included. Each row carries `tree_depth` and `parent_id` (the parent's `id`), which is enough to rebuild the tree. `GET /{slug}/boards/{board_token}/children` returns only the direct children.

```
curl "https://api.playbook.com/v1/coolclient-ltd/boards/boardToken111/descendants?per_page=100" \

  -H "Authorization: Bearer YOUR_TOKEN" | jq
```

***

## List Assets in a Board[​](#list-assets-in-a-board "Direct link to List Assets in a Board")

### Endpoint[​](#endpoint-2 "Direct link to Endpoint")

```
GET /{slug}/boards/{board_token}/assets
```

### Authentication[​](#authentication-2 "Direct link to Authentication")

Send `Authorization: Bearer YOUR_TOKEN` with the request.

### Query Parameters[​](#query-parameters-2 "Direct link to Query Parameters")

* `page` (integer, optional)
* `per_page` (integer, optional)
* `nested_assets` (boolean, optional) — include assets from the board's **entire subtree**, not just the ones sitting directly in it. Requires a board (either this path or `?collection_token=`). Reach for it whenever the token might be a parent board: a board's reported `asset_count` counts the whole subtree, so without this flag a parent whose assets all live in sub-boards reports assets and returns none.
* `get_ai_payload` (boolean, optional) — include each asset's `ai_agent_payload`. Defaults to `false`, and the key is then **absent** from the row rather than `null`, so "I didn't ask for it" stays distinguishable from "this asset has none". Fetching a single asset by token always returns it. See [Agent Context](/docs/guides/upload.md#agent-context-ai_agent_payload-and-ai_generated).
* `created_after` (ISO 8601 timestamp or date, optional) — only assets uploaded strictly after this instant. A bare date (`2026-09-22`) means midnight UTC; an empty value means no filter, and an unparseable one is a `422`. See [What's New Since My Last Run](#whats-new-since-my-last-run).

### Example Request[​](#example-request-2 "Direct link to Example Request")

```
curl "https://api.playbook.com/v1/coolclient-ltd/boards/boardToken1234/assets?page=1&per_page=20" \

  -H "Authorization: Bearer YOUR_TOKEN" | jq
```

### Example Response[​](#example-response-2 "Direct link to Example Response")

```
{

  "data": [

    {

      "id": 987,

      "token": "assetToken111",

      "title": "hero.jpg",

      "display_url": "https://cdn.playbook.com/hero.jpg"

    },

    {

      "id": 988,

      "token": "assetToken222",

      "title": "logo_dark.png",

      "display_url": "https://cdn.playbook.com/logo_dark.png"

    }

  ],

  "pagy": { "current_page": 1, "page_items": 2, "total_pages": 1, "total_count": 2 }

}
```

### What's New Since My Last Run[​](#whats-new-since-my-last-run "Direct link to What's New Since My Last Run")

For a nightly sync, ask for the new uploads in a folder and everything under it, passing the time your **previous run started**:

```
curl "https://api.playbook.com/v1/coolclient-ltd/assets?collection_token=boardToken111&nested_assets=true&created_after=2026-09-22T02:00:00Z" \

  -H "Authorization: Bearer YOUR_TOKEN" | jq
```

`created_after` sees **new uploads only**. An asset that was edited, retagged, or moved into the board from elsewhere keeps its original upload time and is not returned, and deletions are not reported. The listing returns groups rather than their members, so an upload that auto-grouping stacked into a group that already existed (the third or later file with the same name in the same board) is not returned either. For those, pair it with an occasional full crawl (`cursor=true`), or subscribe to the `asset_added` / `asset_updated` / `asset_deleted` [webhooks](/docs/guides/webhooks.md) — `asset_added` fires for every upload, grouped or not.

An empty `created_after=` means no filter; an unparseable value is a `422`.

***

## Read Many Assets at Once[​](#read-many-assets-at-once "Direct link to Read Many Assets at Once")

A listing row already carries `description`, `colors`, `source_url`, custom `fields`, dimensions and comment counts, so a listing needs no second read. When you instead hold a set of tokens (from search, a webhook, your own store), read up to 100 of them in a single request rather than one at a time.

### Endpoint[​](#endpoint-3 "Direct link to Endpoint")

```
POST /{slug}/assets/batch_show
```

It is a `POST` only because the token list travels in the body; nothing is modified.

### Body[​](#body "Direct link to Body")

* `asset_tokens` (array of strings, required) — 1 to 100 tokens.

### Query Parameters[​](#query-parameters-3 "Direct link to Query Parameters")

* `get_ai_payload` (boolean, optional) — include each asset's `ai_agent_payload`. Off by default here, unlike fetching a single asset by token, because 100 payloads of up to 4 KB each is a large response.

### Example Request[​](#example-request-3 "Direct link to Example Request")

```
curl -X POST "https://api.playbook.com/v1/coolclient-ltd/assets/batch_show" \

  -H "Authorization: Bearer YOUR_TOKEN" \

  -H "Content-Type: application/json" \

  -d '{"asset_tokens": ["assetToken111", "assetToken222", "goneToken"]}' | jq
```

### Example Response[​](#example-response-3 "Direct link to Example Response")

```
{

  "data": {

    "assets": [

      {

        "token": "assetToken111",

        "title": "hero.jpg",

        "size": 284913,

        "source_url": "https://supplier.example/hero.jpg",

        "display_url": "https://cdn.playbook.com/hero.jpg"

      },

      {

        "token": "assetToken222",

        "title": "logo_dark.png",

        "size": 18220,

        "source_url": null,

        "display_url": "https://cdn.playbook.com/logo_dark.png"

      }

    ],

    "not_found": ["goneToken"]

  }

}
```

**Always check `not_found`.** One unreadable token does not fail the call, so a token that did not resolve is otherwise indistinguishable from one you forgot to send. A token lands there when it is unknown, deleted, or not visible to you — the three are not distinguished on purpose.

Unlike the assets listing, this addresses assets **by token**, so a grouped child asset is returned when you ask for it. The listing returns only top-level rows: a group's `first_displayable_child` gives its first member, and `GET /{slug}/assets/{token}/children` lists them all (see [Retrieving Group Children](/docs/guides/asset_management.md#retrieving-group-children)).

### About `source_url`[​](#about-source_url "Direct link to about-source_url")

`source_url` is where an asset came from: the URL it was ingested from, for a saved link and for a file Playbook fetched from a URL alike (the browser extension does the latter). It is `null` for direct uploads, notes and colour swatches, which have no origin.

It is **not** a URL you can display or download from — use `thumbnails`, `display_url` or `permalink` for that.

### Playing a video: `stream_url`[​](#playing-a-video-stream_url "Direct link to playing-a-video-stream_url")

Video assets carry `stream_url`, a signed HLS manifest (`master.m3u8`) you can hand straight to any HLS player — hls.js, Safari, VLC, ffplay. The manifest is the whole contract: the quality playlists and segments underneath it are authorized for you already, so there is nothing else to sign.

`stream_url` is a **sibling** of `display_url`, not a replacement. For a video `display_url` stays the poster image, which is what you want for a grid or a `<video poster=…>`. Both are signed for roughly 24 hours, exactly like `thumbnails`.

It is `null` for anything that is not a video, and for a video whose encode has not finished. What you do about a `null` depends on the workspace's plan:

| Plan                                               | What happens                                                                                                                                                                                            |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Team, Business, Enterprise, and Pro or Team trials | The encode starts when the video is uploaded. Read the asset again in a few minutes and `stream_url` is there.                                                                                          |
| Free, Pro, Creative                                | Nothing is encoded up front. **Reading the asset is what starts the encode** — so the first `GET /v1/{slug}/assets/{token}` returns `null` and also queues the work. Read it again a few minutes later. |

Either way the only call you need is the ordinary asset read, and polling it is how you wait.

If `stream_url` never fills in, something went wrong — the encode failed, or on a plan that encodes at upload it was never started. A `null` on its own does not tell you which, so the way to ask is:

```
curl -X POST "https://api.playbook.com/v1/$ORG/assets/$ASSET/retry_video_processing" \

  -H "Authorization: Bearer $TOKEN"
```

A success means an encode has now been queued — go back to polling the asset. A `422` means there is nothing to do: the encode already succeeded, or the workspace's plan does not include video streaming.

**Call it once, not in a loop.** It cannot tell a running encode from a dead one, so calling it again while one is under way queues a second job for the same video. Poll the asset read, never this endpoint.

***

## Error Handling[​](#error-handling "Direct link to Error Handling")

* `401 Unauthorized`: invalid or missing token.
* `404 Not Found`: invalid `slug` or `board_token`.
* `406 Not Acceptable`: `asset_tokens` missing, empty, or not an array.
* `422 Unprocessable Entity`: invalid query parameters, or more than 100 `asset_tokens`.

***

## Tips & Next Steps[​](#tips--next-steps "Direct link to Tips & Next Steps")

* Use `depth=all`, or `/descendants` on a board, to get nested boards in one call.
* Add `displayable_assets=false` to board reads when you only need the tree: large pages return much faster without the previews.
* Use `created_after` with `nested_assets=true` for incremental syncs of new uploads.
* Use `nested_assets=true` when the board token might be a parent — otherwise you get only what sits directly in it.
* List first, then `batch_show` the tokens you care about: two calls for a whole board, rather than one per asset.
* Combine these endpoints to build dashboards or synced local caches of Playbook content.
