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.406—tokensarray 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
| Behavior | move_assets | copy_assets |
|---|---|---|
| Source assets removed | Yes | No |
| Sync vs async | Synchronous (200) | Async (202; background worker) |
| Permission required | Owner of source assets | Read 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 endpoint | Lifetime | Use it for |
|---|---|---|
thumbnails[].url / display_url | Signed, until the end of the next day (24–48h) | Immediate display and download in an active integration. Refresh through ordinary asset reads. |
POST /assets/transform | Until 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. |
permalink | Never expires | URLs stored in your CMS, database, email templates, or other long-lived external systems. |
POST /assets/permalink_urls | Never expires | Reading permalinks back, optionally at a given width/height. |
download_url | Optional and often null | Legacy/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.
Permalinks
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.
Creating Permalinks
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 a406. - Only real uploaded files get one. A link asset, a note, a colour swatch, or an upload still
ingesting (
is_skeleton: true) comes back withpermalink: nulland nothing published. Pollget_assetuntil 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.permalinksreports the headroom left after the call, andGET /v1/organizationsreports the same figure underlimits.permalinkswithout 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.
502means 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. A404here always means an asset token we could not find.
Reading Permalinks Back, at a Size
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.
Permalink Use Cases
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

Removing Permalinks
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.
Creating a Shared Link
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"
}
}
Share vs. Permalink
| Feature | Shared Link | Permalink |
|---|---|---|
| Expiration | Can be revoked | Permanent |
| Password Protection | Yes (via UI) | No |
| Analytics | View tracking | Basic metrics |
| Best For | Client reviews, approvals | Long-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
- Group Thoughtfully: Don't over-group. Keep groups to logically related assets.
- Tag Consistently: Establish tagging conventions early and document them.
- Permalinks for Stability: Use permalinks for external references that need to remain stable.
- Shared Links for Collaboration: Use shared links for temporary external access.
- Audit Regularly: Periodically review and clean up unused groups, tags, and permalinks.
Related API Endpoints
- Group Assets
- Ungroup Assets
- Move Assets
- Copy Assets
- Change Tags
- Add Permalinks
- Remove Permalinks
- Share Asset
- Asset Children
Next Steps
- Learn about Custom Fields for advanced metadata
- Explore Webhooks to automate asset workflows
- Read about AI Search for intelligent asset discovery