# Advanced Asset Management

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

## Prerequisites[​](#prerequisites "Direct link to 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[​](#asset-grouping "Direct link to Asset Grouping")

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

### Creating Asset Groups[​](#creating-asset-groups "Direct link to 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[​](#use-cases-for-asset-groups "Direct link to 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[​](#ungrouping-assets "Direct link to 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[​](#retrieving-group-children "Direct link to 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](/docs/guides/mcp.md) 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[​](#bulk-move-and-copy "Direct link to 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[​](#bulk-move "Direct link to 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[​](#bulk-copy "Direct link to 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[​](#move-vs-copy-at-a-glance "Direct link to 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[​](#tagging-assets "Direct link to Tagging Assets")

Tags help categorize and filter assets for better organization.

### Adding Tags to Assets[​](#adding-tags-to-assets "Direct link to 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[​](#tag-management-best-practices "Direct link to 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[​](#searching-by-tags "Direct link to 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-and-shared-page-visibility "Direct link to 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[​](#choosing-an-asset-url "Direct link to 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[​](#downloading-the-original-file "Direct link to 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](#permalinks).

Over [MCP](/docs/guides/mcp.md) this is `get_asset_download_urls`, which takes up to 25 asset tokens a call.

## Permalinks[​](#permalinks "Direct link to 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[​](#creating-permalinks "Direct link to 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 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[​](#publishing-in-the-background "Direct link to 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[​](#reading-permalinks-back-at-a-size "Direct link to 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[​](#permalink-use-cases "Direct link to 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**

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

### Removing Permalinks[​](#removing-permalinks "Direct link to 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[​](#sharing-assets "Direct link to Sharing Assets")

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

### Creating a Shared Link[​](#creating-a-shared-link "Direct link to 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[​](#share-vs-permalink "Direct link to 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[​](#deleting-assets "Direct link to Deleting Assets")

Remove assets from your organization permanently.

### Delete a Single Asset[​](#delete-a-single-asset "Direct link to 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[​](#batch-deletion "Direct link to 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[​](#important-notes "Direct link to 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[​](#complete-asset-management-workflow "Direct link to 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[​](#error-handling "Direct link to Error Handling")

### Common Errors[​](#common-errors "Direct link to 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[​](#best-practices "Direct link to 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.

***

## Related API Endpoints[​](#related-api-endpoints "Direct link to Related API Endpoints")

* [Group Assets](/docs/api/group-assets.md)
* [Ungroup Assets](/docs/api/ungroup-assets.md)
* [Move Assets](/docs/api/move-assets.md)
* [Copy Assets](/docs/api/copy-assets.md)
* [Change Tags](/docs/api/change-asset-tags.md)
* [Add Permalinks](/docs/api/create-asset-permalinks.md)
* [Remove Permalinks](/docs/api/delete-asset-permalinks.md)
* [Share Asset](/docs/api/share-organization-asset.md)
* [Asset Children](/docs/api/asset-children.md)

***

## Next Steps[​](#next-steps "Direct link to Next Steps")

* Learn about [Custom Fields](/docs/guides/custom_fields.md) for advanced metadata
* Explore [Webhooks](/docs/guides/webhooks.md) to automate asset workflows
* Read about [AI Search](/docs/guides/ai_search.md) for intelligent asset discovery
