Fetching Data from the Playbook API
Retrieve key resources — organizations, boards, and assets—using simple GET calls.
Prerequisites
- Access token: a valid Playbook API token with read permissions, sent in the
Authorization: Bearerheader. - Organization Slug (
slug): Your organization's unique identifier. - Board Token (
board_token): Token of a specific board (for assets endpoint).
List Your Organizations
Endpoint
GET /organizations
Authentication
Send Authorization: Bearer YOUR_TOKEN with the request.
Query Parameters
page(integer, optional, default: 1)per_page(integer, optional, default: 20)
Example Request
curl "https://api.playbook.com/v1/organizations?page=1&per_page=20" \
-H "Authorization: Bearer YOUR_TOKEN" | jq
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
Endpoint
GET /{slug}/boards
Authentication
Send Authorization: Bearer YOUR_TOKEN with the request.
Query Parameters
page(integer, optional)per_page(integer, optional)depth(integer, optional) — fetch nested children up to this level
Example Request
curl "https://api.playbook.com/v1/coolclient-ltd/boards?page=1&per_page=20" \
-H "Authorization: Bearer YOUR_TOKEN" | jq
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 Assets in a Board
Endpoint
GET /{slug}/boards/{board_token}/assets
Authentication
Send Authorization: Bearer YOUR_TOKEN with the request.
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 reportedasset_countcounts 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'sai_agent_payload. Defaults tofalse, and the key is then absent from the row rather thannull, 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.
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
{
"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 }
}
Read Many Assets at Once
Listing rows are deliberately lean. When you need the full record — description, colors,
source_url, custom fields, dimensions, comment counts — ask for up to 100 assets in a single
request instead of fetching them one at a time.
Endpoint
POST /{slug}/assets/batch_show
It is a POST only because the token list travels in the body; nothing is modified.
Body
asset_tokens(array of strings, required) — 1 to 100 tokens.
Query Parameters
get_ai_payload(boolean, optional) — include each asset'sai_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
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
{
"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, since a group's children are reachable
through its first_displayable_child.
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
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
401 Unauthorized: invalid or missing token.404 Not Found: invalidslugorboard_token.406 Not Acceptable:asset_tokensmissing, empty, or not an array.422 Unprocessable Entity: invalid query parameters, or more than 100asset_tokens.
Tips & Next Steps
- Use
depthwhen listing boards to fetch nested child boards in one call. - Use
nested_assets=truewhen the board token might be a parent — otherwise you get only what sits directly in it. - List first, then
batch_showthe 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.