Skip to main content

Advanced Asset Management

Learn how to organize, group, tag, and share assets programmatically using the Playbook API.

Prerequisites​

  • Access Token: API token with asset management permissions
  • Organization Slug: Your organization identifier
  • Asset Tokens: IDs of assets you want to manage

Asset Grouping​

Group multiple assets together to create variants, versions, or related content collections.

Creating Asset Groups​

Group assets under a parent asset to create a cohesive set:

curl -X POST "https://api.playbook.com/v1/my-org/assets/group_assets" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"group": {
"parent_asset_token": "hero-image-main",
"child_asset_tokens": ["hero-variant-1", "hero-variant-2", "hero-variant-3"]
}
}'

Response:

{
"data": {
"id": 123,
"token": "hero-image-main",
"title": "Hero Image",
"is_group": true,
"group_id": null
}
}

Use Cases for Asset Groups​

1. Image Variants

  • Group different sizes/formats of the same image
  • Mobile, desktop, and retina versions
  • Different color variations

2. Version Control

  • Track iterations of design work
  • Group drafts with final versions
  • Maintain revision history

3. Related Content

  • Package assets that belong together
  • Group photos from the same shoot
  • Organize components of a larger project

Ungrouping Assets​

Remove assets from their groups when you need to separate them:

curl -X POST "https://api.playbook.com/v1/my-org/assets/ungroup_assets" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"group": {
"child_asset_tokens": ["hero-variant-1", "hero-variant-2"]
}
}'

Response:

{
"data": [
{
"id": 124,
"token": "hero-variant-1",
"is_group": false,
"group_id": null
},
{
"id": 125,
"token": "hero-variant-2",
"is_group": false,
"group_id": null
}
]
}

Retrieving Group Children​

Asset listings and searches return only top-level rows, so a group's members are read from the group itself. The response is paginated (page, per_page), and a token that is not a group — is_group is false — answers 406 "Not a group". Over MCP this is list_group_children.

Get all assets within a group:

curl "https://api.playbook.com/v1/my-org/assets/hero-image-main/children" \
-H "Authorization: Bearer YOUR_TOKEN"

Response:

{
"data": [
{
"id": 124,
"token": "hero-variant-1",
"title": "Hero Mobile",
"group_id": 123,
"group_sort": 1
},
{
"id": 125,
"token": "hero-variant-2",
"title": "Hero Desktop",
"group_id": 123,
"group_sort": 2
}
],
"pagy": {
"current_page": 1,
"total_count": 2
}
}

Bulk Move and Copy​

When you need to relocate or duplicate many assets at once, use the dedicated bulk endpoints instead of looping over single-asset updates. Both accept the same shape: an array of asset tokens and a destination collection_token.

Bulk Move​

move_assets is synchronous. The request returns once all assets have been moved.

curl -X POST "https://api.playbook.com/v1/my-org/assets/move_assets" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"move": {
"tokens": ["asset-tok-1", "asset-tok-2", "asset-tok-3"],
"collection_token": "destination-board"
}
}'

Response: 200 OK. Errors:

  • 403 — the caller is a viewer trying to move assets they don't own.
  • 404 — destination board does not exist.
  • 406 — tokens array is empty.
  • 422 — too many tokens for one request.

Bulk Copy​

copy_assets is asynchronous and returns 202 Accepted immediately. Playbook copies the assets in a background job; poll the destination board to observe new assets appearing, or subscribe to webhook events to be notified.

curl -X POST "https://api.playbook.com/v1/my-org/assets/copy_assets" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"copy": {
"tokens": ["asset-tok-1", "asset-tok-2"],
"collection_token": "destination-board"
}
}'

Response: 202 Accepted. Same error semantics as move_assets, except no 403 (copying does not change the source).

Move vs. Copy at a glance​

Behaviormove_assetscopy_assets
Source assets removedYesNo
Sync vs asyncSynchronous (200)Async (202; background worker)
Permission requiredOwner of source assetsRead on source, write on target

Tagging Assets​

Tags help categorize and filter assets for better organization.

Adding Tags to Assets​

change_tags takes add_tags and remove_tags, and touches only the tags you name — anything already on the asset and absent from both lists is left alone.

curl -X POST "https://api.playbook.com/v1/my-org/assets/logo-png/change_tags" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"add_tags": ["branding", "logo", "approved", "v2"],
"remove_tags": []
}'

Response:

{
"data": {
"id": 200,
"token": "logo-png",
"tags": ["branding", "logo", "approved", "v2"]
}
}

Tag Management Best Practices​

1. Consistent Naming

// Good: lowercase, hyphenated
["brand-assets", "social-media", "high-priority"][
// Avoid: mixed case, spaces
("Brand Assets", "social_media", "High Priority")
];

2. Hierarchical Tags

// Organize with prefixes
["status:approved", "status:needs-review", "status:archived"][
("category:logo", "category:icon", "category:banner")
][("project:website", "project:mobile-app")];

3. Tag-Based Workflows

# Batch update multiple assets
for token in logo-main logo-alt logo-mobile; do
curl -X POST "https://api.playbook.com/v1/my-org/assets/$token/change_tags" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"add_tags": ["branding", "approved", "q4-2024"], "remove_tags": []}'
done

Searching by Tags​

Use the search endpoint with tag filters:

curl --globoff "https://api.playbook.com/v1/my-org/search?query=&filters[tags][]=branding&filters[tags][]=approved&filters[tags_op]=and" \
-H "Authorization: Bearer YOUR_TOKEN"

Approval and Shared-Page Visibility​

Approval controls whether assets appear on shared and published pages. Workspace owners and admins can update up to 1000 assets in one call, including assets from different boards:

curl -X POST "https://api.playbook.com/v1/my-org/assets/set_approval" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tokens": ["draft-hero", "draft-logo"],
"approved": false,
"approved_after": "2026-08-25T12:00:00Z"
}'

Set approved: true to make assets visible immediately. Set it to false to hide them; optionally include a future approved_after timestamp to make them visible automatically. The response's approved field is the current effective state. approved_after is the configured threshold and remains present after it passes, so do not use a non-null timestamp as evidence that an asset is still hidden.

Choosing an Asset URL​

Asset responses expose URLs with different lifetimes:

Field or endpointLifetimeUse it for
thumbnails[].url / display_urlSigned, until the end of the next day (24–48h)Immediate display and download in an active integration. Refresh through ordinary asset reads.
POST /assets/transformUntil the end of the next hour (1–2h)A resized or reformatted render of an image. display_url is the full-size one — often 5000px wide — so ask for a width when you need a thumbnail.
permalinkNever expiresURLs stored in your CMS, database, email templates, or other long-lived external systems.
POST /assets/permalink_urlsNever expiresReading permalinks back, optionally at a given width/height.
GET /assets/{token}/downloadSigned, about a dayThe original file, byte for byte, as raw_url — a full-quality video, a PDF, a font, a source image. Every other URL here is a re-encoded render.
download_urlOptional and often nullLegacy/specialized download flows; do not depend on it being present.

Downloading the Original File​

display_url, thumbnails, transforms and image permalinks are renders. When another system needs the file that was uploaded — an ad platform ingesting a video by URL, a script that reads a CSV — ask for it:

curl "https://api.playbook.com/v1/my-org/assets/launch-video/download" \
-H "Authorization: Bearer YOUR_TOKEN"

Response:

{
"data": {
"display_url": "https://cdn.playbook.com/launch-video-poster.webp",
"raw_url": "https://cdn.playbook.com/launch-video-original.mp4",
"size": 48211034
}
}
  • raw_url needs no login and expires within about a day. For a link asset it is the address the link points to; for an asset with no file of its own (a note, a color swatch, a group, an upload still ingesting) it is null.
  • The caller needs download permission on the asset.
  • Each call is one monthly API request, so use it when you need the file. Do not poll it to refresh an image view: the signed fields on ordinary asset responses have the same lifetime.
  • For a URL that must keep working longer than a day, create a permalink.

Over MCP this is get_asset_download_urls, which takes up to 25 asset tokens a call.

A permalink is a permanent, public URL for an asset's file: no signature, no expiry, no login. It is what you store in a CMS, an email template, or anywhere else the link has to keep working. Publishing copies the file to a public bucket, so treat it as making the file public to anyone who has the URL.

curl -X POST "https://api.playbook.com/v1/my-org/assets/add_permalinks" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"asset_tokens": ["logo-png", "banner-jpg", "icon-svg"]}'

Response:

{
"data": [
{
"id": 200,
"token": "logo-png",
"permalink": "https://img.playbook.com/PmSGkxSc.../rs:fit:0:0/abc123def.webp"
},
{
"id": 201,
"token": "banner-jpg",
"permalink": "https://img.playbook.com/8jQzT1vY.../rs:fit:0:0/def456ghi.webp"
}
],
"meta": {
"permalinks": { "type": "count", "left": 997, "total": 1000 }
}
}

Things worth knowing before you wire this up:

  • The response covers only the assets it changed. Assets that already had a permalink are left alone and are absent from data — read theirs from any ordinary asset response. If every token you sent already had one, the call is a 406.
  • Only real uploaded files get one. A link asset, a note, a colour swatch, or an upload still ingesting (is_skeleton: true) comes back with permalink: null and nothing published. Poll get_asset until the upload finishes before asking.
  • Permalinks are capped per plan — 10 on Free, 100 on trials, 1,000 on Pro, 5,000 on Team and Business. Going over refuses the whole batch with a 422; it does not publish the part that fits. meta.permalinks reports the headroom left after the call, and GET /v1/organizations reports the same figure under limits.permalinks without publishing anything.
  • Images are served through the image pipeline, everything else from the permalink host — so the URL shape differs by media type. Store whatever the API returns; never build one yourself.
  • 502 means publishing failed on our side, not that anything is wrong with your request: the file could not be copied into the public bucket. The message says how many assets of the batch made it before the failure — those keep their permalinks — so retry the rest rather than the whole list. A 404 here always means an asset token we could not find.
  • This call copies every file before it answers, so large files (video) or large batches can outlast the request. For those, publish in the background instead.

Publishing in the Background​

Add async=true and the call answers at once with 202: each permalink is reserved (and counted against the plan cap) immediately, and the files are copied in the background.

curl -X POST "https://api.playbook.com/v1/my-org/assets/add_permalinks?async=true" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"asset_tokens": ["launch-video", "logo-png", "brief-note"]}'
{
"data": {
"pending": ["launch-video", "logo-png"],
"no_file_to_publish": ["brief-note"]
},
"meta": {
"permalinks": { "type": "count", "left": 997, "total": 1000 }
}
}

The response carries tokens, not URLs: a permalink's URL is withheld until its file is in place, because until then it would return 404. Read them with permalink_urls below, and poll the pending ones every few seconds until their status is ready. Small images usually land within seconds; a large video can take minutes. While a permalink is pending, every asset response reports permalink: null for it — publishing it again is a no-op, not a retry. If the file cannot be copied at all, the permalink is withdrawn, its quota slot is freed, and status goes back to none.

Tokens that already had a permalink, the 406 when all of them did, the 422 over the plan cap and the 404 for an unknown token behave exactly as in the synchronous call.

permalink_urls returns the permanent URL of assets that already have one, optionally rendered at a given size. It publishes nothing. Each row carries a status: ready (the URL works), pending (a background publish is still copying the file — the URL is null until it lands) or none (no permalink).

curl -X POST "https://api.playbook.com/v1/my-org/assets/permalink_urls?width=800" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"asset_tokens": ["logo-png", "launch-video", "banner-jpg"]}'
{
"data": {
"permalinks": [
{ "asset_token": "logo-png", "permalink": "https://img.playbook.com/.../rs:fit:800:0/abc123def.webp", "status": "ready" },
{ "asset_token": "launch-video", "permalink": null, "status": "pending" },
{ "asset_token": "banner-jpg", "permalink": null, "status": "none" }
]
}
}

width and height resize images only; pass one of the two to keep the aspect ratio. Every other media type ignores both and is served at its stored size.

1. External Integrations

// Store the permalink the API returned — do not construct one
const asset = {
id: "header-image",
playbookPermalink: permalink,
displayName: "Header Background",
};

2. Email Campaigns

<!-- Permalinks survive in templates that are opened weeks later -->
<img src="{{ permalink }}" alt="Product Banner" />

3. Documentation

![Company Logo](https://img.playbook.com/.../abc123def.webp)

Revoking frees plan quota, and adding a permalink back mints a different URL — there is no way to restore the old one.

It is not a takedown. The asset stops reporting a permalink immediately, but the file already published stays fetchable to anyone holding the URL until the public copy is purged, which runs on a safety delay of about a week. If you need something unreachable right now, revoking is not enough.

curl -X POST "https://api.playbook.com/v1/my-org/assets/remove_permalinks" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"asset_tokens": ["logo-png", "banner-jpg"]}'

Like the create call, this reports only the assets it changed: tokens that had no permalink are skipped, and if none of them had one the call is a 406. The freed-up quota comes back in meta.permalinks.


Sharing Assets​

Create temporary or password-protected shared links for specific assets.

curl -X POST "https://api.playbook.com/v1/my-org/assets/design-mockup/share" \
-H "Authorization: Bearer YOUR_TOKEN"

Response:

{
"data": {
"url": "https://playbook.com/s/xyz789abc/design-mockup"
}
}
FeatureShared LinkPermalink
ExpirationCan be revokedPermanent
Password ProtectionYes (via UI)No
AnalyticsView trackingBasic metrics
Best ForClient reviews, approvalsLong-term references

Deleting Assets​

Remove assets from your organization permanently.

Delete a Single Asset​

curl -X DELETE "https://api.playbook.com/v1/my-org/assets/logo-png" \
-H "Authorization: Bearer YOUR_TOKEN"

Response: 204 No Content

Batch Deletion​

for token in old-banner outdated-logo draft-icon; do
curl -X DELETE "https://api.playbook.com/v1/my-org/assets/$token" \
-H "Authorization: Bearer YOUR_TOKEN"
done

Important Notes​

  • Deleted assets cannot be recovered via the API.
  • Deleting a group parent does not automatically delete its children — ungroup first if needed.
  • Assets in shared or published boards can still be deleted by authorized users.

Complete Asset Management Workflow​

Here's a complete example of managing assets for a product launch:

const PLAYBOOK_API = "https://api.playbook.com";
const ACCESS_TOKEN = "your_token_here";
const ORG_SLUG = "my-org";

async function manageLaunchAssets() {
// 1. Upload product images
const uploadedAssets = await uploadProductImages();

// 2. Group variants together
await fetch(`${PLAYBOOK_API}/v1/${ORG_SLUG}/assets/group_assets`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${ACCESS_TOKEN}`,
},
body: JSON.stringify({
group: {
parent_asset_token: uploadedAssets.main,
child_asset_tokens: uploadedAssets.variants,
},
}),
});

// 3. Tag for organization
const tags = ["product-launch", "q4-2024", "approved"];
for (const token of [uploadedAssets.main, ...uploadedAssets.variants]) {
await fetch(`${PLAYBOOK_API}/v1/${ORG_SLUG}/assets/${token}/change_tags`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${ACCESS_TOKEN}`,
},
body: JSON.stringify({ add_tags: tags, remove_tags: [] }),
});
}

// 4. Create permalinks for marketing team
const permalinkResponse = await fetch(
`${PLAYBOOK_API}/v1/${ORG_SLUG}/assets/add_permalinks`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${ACCESS_TOKEN}`,
},
body: JSON.stringify({ asset_tokens: [uploadedAssets.main] }),
},
);

const { data } = await permalinkResponse.json();
console.log("Permalink created:", data[0].permalink);

// 5. Share with external reviewers
const shareResponse = await fetch(
`${PLAYBOOK_API}/v1/${ORG_SLUG}/assets/${uploadedAssets.main}/share`,
{ method: "POST", headers: { Authorization: `Bearer ${ACCESS_TOKEN}` } },
);

const shareData = await shareResponse.json();
console.log("Share link:", shareData.data.url);
}

Error Handling​

Common Errors​

422: Cannot group asset already in a group

{
"error": "Cannot group into an asset that is already part of a group"
}

Solution: Ungroup the child assets first.

422: Permalink limit exceeded

{
"error": "Permalink limit exceeded for your plan"
}

Solution: Remove unused permalinks or upgrade your plan.

404: Assets not found

{
"error": "Child assets not found"
}

Solution: Verify asset tokens are correct and accessible.


Best Practices​

  1. Group Thoughtfully: Don't over-group. Keep groups to logically related assets.
  2. Tag Consistently: Establish tagging conventions early and document them.
  3. Permalinks for Stability: Use permalinks for external references that need to remain stable.
  4. Shared Links for Collaboration: Use shared links for temporary external access.
  5. Audit Regularly: Periodically review and clean up unused groups, tags, and permalinks.


Next Steps​