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

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.
  • 406tokens 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.
download_urlOptional and often nullLegacy/specialized download flows; do not depend on it being present.

Do not poll the deprecated /v1/{slug}/assets/{token}/download endpoint to refresh every image view. It consumes one monthly API request per call and returns a URL with the same lifetime as the signed fields already present on normal asset responses.

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.

permalink_urls returns the permanent URL of assets that already have one, optionally rendered at a given size. It publishes nothing: an asset without a permalink comes back as null.

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", "banner-jpg"]}'
{
"data": {
"permalinks": [
{ "asset_token": "logo-png", "permalink": "https://img.playbook.com/.../rs:fit:800:0/abc123def.webp" },
{ "asset_token": "banner-jpg", "permalink": null }
]
}
}

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