Uploading Assets to Playbook
You can add assets to Playbook either by pointing at a public URL or by using a two-step signed-upload flow.
Prerequisites
- Access Token (
access_token): OAuth2 token with asset-upload permission. - Organization Slug (
slug): Your org's identifier (e.g.,coolclient-ltd). - Board Token (
board_token): (Optional) Where to place the asset. If omitted, asset goes to a default location (Uploaded todayboard). You can fetch board tokens using the boards endpoint.
Direct Upload from Public URL
Endpoint
POST /api/v1/{slug}/assets
Authorization: Bearer <access_token>
Content-Type: application/json
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| uri | string | yes | Publicly accessible URL to fetch the file. |
| filename | string | yes | Desired filename in Playbook (e.g., image.jpg). |
| title | string | no | Display name for the asset. |
| description | string | no | Optional description or notes. |
| board_token | string | no | Token of the target board. |
| as_link | boolean | no | If true, asset is stored as an external link without processing. |
Sample Request
curl -X POST https://api.playbook.com/api/v1/coolclient-ltd/assets \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"asset": {
"uri": "https://example.com/photo.jpg",
"title": "User Photo",
"collection_token": "homepage-assets"
}
}'
Sample Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {
"id": 101,
"token": "assetToken123",
"display_url": "https://cdn.playbook.com/photo.jpg",
"media_type": "image/jpeg",
"collection_token": "boardToken123",
"is_skeleton": true,
"is_link": false,
"source_error": null,
...
}
}
URL ingest is asynchronous. The response returns immediately with
is_skeleton: true. Playbook fetches the bytes in the background. See Async Ingest Semantics below for how to detect completion.
Async Ingest Semantics
Both POST /api/v1/{slug}/assets (with a uri) and POST /api/v1/{slug}/assets/batch_create_from_urls enqueue a background worker that fetches the bytes from the supplied URL. The HTTP response returns immediately with a placeholder asset (is_skeleton: true). To know when the upload has finished, poll GET /api/v1/{slug}/assets/{asset_token} and inspect the following fields:
| Field | Type | Meaning |
|---|---|---|
is_skeleton | boolean | true while the worker is still fetching. Becomes false when the worker is finished. |
media_type | string | Populated on success (e.g., image/jpeg). |
source_error | string | The latest error message from the URL-download worker. null on success. |
is_link | boolean | true when the asset was stored as a bare link (because as_link: true was set). |
Terminal states
Treat the upload as finished when is_skeleton: false AND one of the following holds:
- Success —
media_typeis populated andsource_errorisnull. - Failed —
source_erroris non-null. The error message describes why the worker could not fetch or process the bytes (e.g., 404 from the source URL, unsupported file type). - Stored as link —
is_link: true(only whenas_link: truewas passed in the request). The asset exists as a bare link with no fetched bytes.
Recommended polling cadence
Most uploads finish within a few seconds. Polling every 1–2 seconds with exponential backoff up to ~60 seconds is sufficient. If is_skeleton is still true after 60 seconds, treat it as a worker delay rather than a failure and continue polling at a slower cadence.
Batch Upload from Public URLs
Use this endpoint to ingest up to 100 public URLs in a single request. All assets land in the same board, the entire batch is wrapped in a database transaction (so a single bad asset rolls back the whole batch), and one UPLOAD_ASSETS event is emitted for the batch.
Endpoint
POST /api/v1/{slug}/assets/batch_create_from_urls
Authorization: Bearer <access_token>
Content-Type: application/json
Requires the write OAuth scope.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
batch.collection_token | string | no | Token of the destination board, applied to every asset in the batch. |
batch.collection_id | integer | no | Numeric ID alternative to collection_token. |
batch.assets | array | yes | 1 to 100 asset specs (see below). |
Each item in batch.assets:
| Field | Type | Required | Description |
|---|---|---|---|
uri | string | yes | Public URL to ingest. |
uuid | string | no | Client-supplied correlation id, returned untouched on the matching response row. |
title | string | no | Override the title (defaults to filename derived from URL). |
description | string | no | Optional description. |
as_link | boolean | no | If true, store as a bare link instead of fetching bytes. |
tags | array | no | Manual tags to apply on creation. |
status | string | no | Status label to set on creation. |
Sample Request
curl -X POST https://api.playbook.com/api/v1/coolclient-ltd/assets/batch_create_from_urls \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"batch": {
"collection_token": "homepage-assets",
"assets": [
{ "uuid": "client-1", "uri": "https://example.com/a.jpg", "title": "Hero" },
{ "uuid": "client-2", "uri": "https://example.com/b.png", "as_link": true }
]
}
}'
Sample Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [
{
"uuid": "client-1",
"asset": {
"token": "asset-tok-1",
"title": "Hero",
"is_skeleton": true,
"is_link": false,
"source_error": null,
...
}
},
{
"uuid": "client-2",
"asset": {
"token": "asset-tok-2",
"is_skeleton": false,
"is_link": true,
"source_error": null,
...
}
}
]
}
The response is an array of { uuid, asset } rows. Each asset starts as a skeleton and reaches its terminal state asynchronously — see Async Ingest Semantics above. Poll GET /api/v1/{slug}/assets/{asset_token} for each asset.
Limits and validation
- Maximum 100 assets per request — exceeding this returns
422. - Every asset must include a
uri— missing or blank returns406. - The org's total-asset limit is enforced — exceeding it returns
422. - All-or-nothing: if any asset fails to be created, the entire batch is rolled back.
Two-Step Upload Flow (Prepare & Complete)
Use this flow when you want to upload large files or have more control over the upload process.
Step 1: Request Upload Credentials
Endpoint
POST /api/v1/{slug}/assets/upload_prepare
Authorization: Bearer <access_token>
Content-Type: application/json
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | yes | File name or display label. |
| media_type | string | yes | MIME type (e.g., video/mp4). |
| size | integer | yes | File byte size. |
Sample Request
curl -X POST https://api.playbook.com/api/v1/coolclient-ltd/assets/upload_prepare \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"asset": {
"title": "Vacation Video",
"media_type": "video/mp4",
"size": 52428800
}
}'
Sample Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {
"upload_url": "https://storage.googleapis.com/playbook-uploads/...",
"signed_gcs_id": "abc123def456",
"file_extension": "mp4"
}
}
Step 2: Upload the File to Storage
Use the upload_url (usually a pre-signed PUT URL) to send your file directly to storage.
curl -X PUT "https://storage.googleapis.com/playbook-uploads/..." \
-H "Content-Type: video/mp4" \
--data-binary @/path/to/Vacation.mp4
Step 3: Complete the Upload
Endpoint
POST /api/v1/{slug}/assets/upload_complete
Authorization: Bearer <access_token>
Content-Type: application/json
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| signed_gcs_id | string | yes | ID returned from the prepare step. |
| title | string | no | (Optional) override title. |
| description | string | no | (Optional) asset description. |
| media_type | string | yes | Same MIME type as the prepare call. |
| size | integer | yes | Byte size (same as prepare). |
| board_token | string | no | Target board token. |
Sample Request
curl -X POST https://api.playbook.com/api/v1/coolclient-ltd/assets/upload_complete \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"asset": {
"signed_gcs_id": "abc123def456",
"title": "Vacation Video",
"media_type": "video/mp4",
"size": 52428800,
"collection_token": "video-collection"
}
}'
Sample Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {
"token": "vacation-video-mp4",
"display_url": "https://cdn.playbook.com/vacation-video.mp4",
"media_type": "video/mp4"
}
}
Batch Two-Step Upload (Prepare & Complete)
For ingesting many large files in one round, the two-step flow above has a batch counterpart. It mirrors the single-asset flow exactly — batch_upload_prepare returns one upload_url per asset and a shared batch_id, then you PUT each file to its URL, then call batch_upload_complete once with all the signed_gcs_id values to materialize the asset records.
Unlike batch_create_from_urls (which is all-or-nothing), batch_upload_complete creates assets individually — a single failure does not roll back already-created assets in the same batch.
Step 1 — batch_upload_prepare
POST /api/v1/{slug}/assets/batch_upload_prepare
Authorization: Bearer <access_token>
Content-Type: application/json
Request:
| Field | Type | Required | Description |
|---|---|---|---|
batch.assets | array | yes | 1 to 100 asset specs (see below). |
Each item in batch.assets:
| Field | Type | Required | Description |
|---|---|---|---|
title | string | yes | Filename of the asset. |
size | integer | yes | File size in bytes. |
uuid | string | yes | Client-generated correlation id, returned untouched in the response. |
media_type | string | no | MIME type (inferred from title extension if omitted). |
Sample request:
curl -X POST https://api.playbook.com/api/v1/coolclient-ltd/assets/batch_upload_prepare \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"batch": {
"assets": [
{ "uuid": "client-1", "title": "photo.jpg", "size": 1024000, "media_type": "image/jpeg" },
{ "uuid": "client-2", "title": "doc.pdf", "size": 5242880, "media_type": "application/pdf" }
]
}
}'
Sample response:
{
"data": {
"batch_id": "uuid-of-batch",
"assets": [
{
"uuid": "client-1",
"upload_url": "https://storage.googleapis.com/...",
"signed_gcs_id": "abc...",
"file_extension": "jpg",
"storage_provider": "gcs"
},
{
"uuid": "client-2",
"upload_url": "https://storage.googleapis.com/...",
"signed_gcs_id": "def...",
"file_extension": "pdf",
"storage_provider": "gcs"
}
]
}
}
Step 2 — Upload each file
PUT each file to its upload_url, the same as in the single-asset two-step flow. Run uploads in parallel for throughput.
Step 3 — batch_upload_complete
POST /api/v1/{slug}/assets/batch_upload_complete
Authorization: Bearer <access_token>
Content-Type: application/json
Request:
| Field | Type | Required | Description |
|---|---|---|---|
batch.batch_id | string | no | Value from batch_upload_prepare. Recommended — releases the upload reservation. |
batch.collection_token | string | no | Destination board for every asset in the batch. |
batch.collection_id | integer | no | Numeric alternative to collection_token. |
batch.assets | array | yes | 1 to 100 completion specs (see below). |
Each item in batch.assets:
| Field | Type | Required | Description |
|---|---|---|---|
uuid | string | yes | Same client uuid you sent in batch_upload_prepare. |
signed_gcs_id | string | yes | Exact value returned in the matching prepare response row. |
title | string | yes | Display title for the asset. |
description | string | no | Optional description. |
media_type | string | no | MIME type — same value used in prepare. |
width / height | integer | no | Image dimensions, when known client-side. |
multipart_upload_id | string | no | Required only for Backblaze multipart uploads. |
Sample request:
curl -X POST https://api.playbook.com/api/v1/coolclient-ltd/assets/batch_upload_complete \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"batch": {
"batch_id": "uuid-of-batch",
"collection_token": "homepage-assets",
"assets": [
{ "uuid": "client-1", "signed_gcs_id": "abc...", "title": "photo.jpg",
"media_type": "image/jpeg", "width": 1920, "height": 1080 },
{ "uuid": "client-2", "signed_gcs_id": "def...", "title": "doc.pdf",
"media_type": "application/pdf" }
]
}
}'
Sample response:
{
"data": [
{ "uuid": "client-1", "asset": { "token": "asset-tok-1", "media_type": "image/jpeg", ... } },
{ "uuid": "client-2", "asset": { "token": "asset-tok-2", "media_type": "application/pdf", ... } }
]
}
When to choose which flow
| Use case | Endpoint |
|---|---|
| Asset bytes already live at a public URL | batch_create_from_urls |
| Files live on the client (browser, server, CI) | batch_upload_prepare + batch_upload_complete |
| One file at a time | assets (URL) or upload_prepare + upload_complete (signed) |
Error Handling
400 Bad Request: Missing or malformed JSON fields.401 Unauthorized: Invalid or missingaccess_token.403 Forbidden: Token lacks thewritescope, or the user lacksupdatepermission on the target board.404 Not Found(batch complete): One or moresigned_gcs_idvalues point to objects not present in storage — re-upload before retrying.406 Not Acceptable: A required field is missing (e.g., a batch asset withouturi).422 Unprocessable Entity: File size/type mismatch, expired upload URL, batch size out of bounds (must be 1–100), or workspace asset limit exceeded.
Tips
- For very large files, monitor your upload progress and retry on failures.
- Clean up or retry failed
signed_gcs_ids by re-calling the prepare step. - Track asset
tokenfor later operations like update or delete.