Skip to main content

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

  1. Access Token (access_token): OAuth2 token with asset-upload permission.
  2. Organization Slug (slug): Your org's identifier (e.g., coolclient-ltd).
  3. Board Token (board_token): (Optional) Where to place the asset. If omitted, asset goes to a default location (Uploaded today board). 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

FieldTypeRequiredDescription
uristringyesPublicly accessible URL to fetch the file.
filenamestringyesDesired filename in Playbook (e.g., image.jpg).
titlestringnoDisplay name for the asset.
descriptionstringnoOptional description or notes.
board_tokenstringnoToken of the target board.
as_linkbooleannoIf 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:

FieldTypeMeaning
is_skeletonbooleantrue while the worker is still fetching. Becomes false when the worker is finished.
media_typestringPopulated on success (e.g., image/jpeg).
source_errorstringThe latest error message from the URL-download worker. null on success.
is_linkbooleantrue 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:

  • Successmedia_type is populated and source_error is null.
  • Failedsource_error is 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 linkis_link: true (only when as_link: true was passed in the request). The asset exists as a bare link with no fetched bytes.

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

FieldTypeRequiredDescription
batch.collection_tokenstringnoToken of the destination board, applied to every asset in the batch.
batch.collection_idintegernoNumeric ID alternative to collection_token.
batch.assetsarrayyes1 to 100 asset specs (see below).

Each item in batch.assets:

FieldTypeRequiredDescription
uristringyesPublic URL to ingest.
uuidstringnoClient-supplied correlation id, returned untouched on the matching response row.
titlestringnoOverride the title (defaults to filename derived from URL).
descriptionstringnoOptional description.
as_linkbooleannoIf true, store as a bare link instead of fetching bytes.
tagsarraynoManual tags to apply on creation.
statusstringnoStatus 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 returns 406.
  • 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

FieldTypeRequiredDescription
titlestringyesFile name or display label.
media_typestringyesMIME type (e.g., video/mp4).
sizeintegeryesFile 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

FieldTypeRequiredDescription
signed_gcs_idstringyesID returned from the prepare step.
titlestringno(Optional) override title.
descriptionstringno(Optional) asset description.
media_typestringyesSame MIME type as the prepare call.
sizeintegeryesByte size (same as prepare).
board_tokenstringnoTarget 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"
}
}

Error Handling

  • 400 Bad Request: Missing or malformed JSON fields.
  • 401 Unauthorized: Invalid or missing access_token.
  • 403 Forbidden: Token lacks the write scope, or the user lacks update permission on the target board.
  • 406 Not Acceptable: A required field is missing (e.g., a batch asset without uri).
  • 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 token for later operations like update or delete.