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—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. |
GET /assets/{token}/download | Signed, about a day | The 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_url | Optional and often null | Legacy/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_urlneeds 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 isnull.- 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.
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.- 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.
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. 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.
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