Asset Versioning
Playbook keeps a version history for every asset. Replacing an asset's file does not lose the old one: the previous state is snapshotted first, and you can list what came before and roll back to it.
The live asset row is always the head — the current content, reachable at its usual token and URLs. Everything before it is stored as an immutable snapshot. Because the asset keeps its token, its boards, and its shared links across a version write, links you have already handed out keep working and start serving the new content.
Prerequisites
- Access Token: an API token with the
writescope for anything that changes a version. Reading version history needs onlyread. - Organization Slug: your workspace identifier.
- Asset Token: the asset whose history you want.
How version numbers work
Each stored version has a monotonic version_number, and that number is the stable handle you pass
to revert or to annotate.
head_version_number is the number the next version write will take — not the newest stored
version. An asset that has never been versioned reports head_version_number: 1. The newest stored
version is always head_version_number - 1.
This trips people up most often on revert, so it is worth stating outright:
Revert moves the head forward, not backward. Reverting to version 2 does not set the head back to 2. It snapshots the current head as a new version, then rotates the asset's bytes to version 2's content. The returned
head_version_numberisold_head + 1.
That is what makes the operation non-destructive: the state you reverted away from is still in the history, so you can revert back to it.
List version history
Newest first. Available on every plan, with only the read scope.
curl "https://api.playbook.com/v1/my-org/assets/hero-image/versions" \
-H "Authorization: Bearer YOUR_TOKEN"
Response:
{
"data": [
{
"version_number": 3,
"source": "REVERT",
"comment": "Reverted to the approved crop",
"md5": "9f2c...",
"size": 482113,
"media_type": "image/jpeg",
"revertable": true,
"created_at": "2026-08-14T10:12:04Z",
"created_by_token": "user-abc",
"thumbnails": []
},
{
"version_number": 2,
"source": "UPLOAD",
"comment": null,
"revertable": true,
"created_at": "2026-08-02T09:41:55Z"
}
],
"head_version_number": 4,
"pagy": { "current_page": 1, "total_count": 3 }
}
Note that head_version_number is a top-level key on the envelope, a sibling of data — not a
field inside a version row.
Revert to an earlier version
Requires the write scope.
curl -X POST "https://api.playbook.com/v1/my-org/assets/hero-image/revert" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"version_number": 2,
"comment": "Client preferred the original crop"
}'
The optional comment is recorded on the snapshot of the head being replaced, not on the content you
are restoring.
Reverting to the live head is a benign no-op: it returns 200, creates no new version, and leaves
the head where it was. That also means a client that times out and retries a revert that already
succeeded does not stack up duplicate versions.
Upload new bytes as a new version
There is no separate "new version" endpoint. Versioning new bytes is a mode of the normal asset
update, selected with create_version. First reserve an upload with upload_prepare and send the
bytes exactly as you would for a new asset — on GCS that means POSTing to upload_url to open a
resumable session and PUTting the bytes to the Location it returns; see
Uploading Assets for the full handshake. Then finalize against the existing asset:
curl -X PATCH "https://api.playbook.com/v1/my-org/assets/hero-image" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"asset": {
"create_version": true,
"signed_gcs_id": "SIGNED_ID_FROM_UPLOAD_PREPARE",
"media_type": "image/jpeg",
"comment": "Reshot at higher resolution"
}
}'
Sending create_version: true without signed_gcs_id takes a metadata-only checkpoint — but
only if the same request actually changes something. The server snapshots the head when the update
changes title, description, or text, or when you pass a non-empty comment. A bare
{"asset": {"create_version": true}} changes nothing, so it returns 200 and creates no
version. If you want a checkpoint without editing the asset, pass a comment.
Edit a version's note
Send version_number without signed_gcs_id and the update is interpreted as a comment edit on
that one version. No new version is created and the asset's content is untouched.
curl -X PATCH "https://api.playbook.com/v1/my-org/assets/hero-image" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "asset": { "version_number": 2, "comment": "Approved by brand review" } }'
Pass an empty string to clear a note. Omitting comment entirely is a no-op.
Plans and the revert window
History itself is unlimited on every plan, including Free. Version rows are never pruned and there is no cap on how many an asset may accumulate. Listing history always works.
What is gated is how far back you can restore:
| Plan | Revert window |
|---|---|
| Free, Pro | 7 days from the version's creation |
| Team, Business, Enterprise | Unlimited |
Trial plans inherit the window of the paid plan they are trialling. The window is measured from each
version's created_at, and each version in the list carries a server-computed revertable flag so
you do not have to work it out yourself — prefer that flag over computing the age yourself. If you do
attempt a revert outside the window, the 403 response carries the workspace's window length as
extensions.revert_window_days.
On a gated plan, versions older than the window also withhold the full-resolution original: their
primary_gcs_id comes back null. The two surfaces differ in what they return instead, so do not
assume one from the other:
- Over REST, thumbnails for a past-window version are still returned, watermarked and clamped to a preview size.
- Over MCP,
list_asset_versionsreturnsthumbnails: []for a past-window version — the tool drops them rather than passing the watermarked set through.
Either way, use the live asset's display_url to preview current content.
Error codes
Errors use the standard envelope, { "errors": [{ "message": "...", "extensions": { "code": "..." } }] }.
Branch on extensions.code rather than on the message text:
| Code | Status | Meaning |
|---|---|---|
versioning_not_enabled | 403 | The workspace plan does not allow version writes. |
version_write_forbidden | 403 | The asset's board is not version-writeable — trashed, archived, read-only, or the asset is not on a board. Not a token problem. |
revert_window_exceeded | 403 | Target is older than the plan's revert window. Also carries revert_window_days. |
version_not_found | 404 | No such version_number — above the head, or a gap. |
invalid_version_number | 422 | Not a positive integer. |
version_blob_missing | 422 | The stored file is no longer available in storage. |
From an AI assistant
Every operation above is available to AI assistants over the hosted MCP server, so you can drive version history in natural language instead of writing calls:
| Tool | What it does |
|---|---|
list_asset_versions | Version history, newest first, plus head_version_number. Read-only. |
revert_asset | Revert to a version_number. Needs write. |
create_asset_version | Upload a public URL as a new version of an existing asset. Needs write. |
edit_asset_version_comment | Edit the note on an existing version. Needs write. |
create_asset_version takes a public https:// URL rather than raw bytes, and refuses files that
would need a multipart upload — use the web or desktop app for those.
Related
- Advanced Asset Management — grouping, tagging, moving, and batch operations.
- Uploading Assets — the
upload_prepareflow referenced above. - Connect AI Assistants via MCP — connecting Claude, Cursor, or ChatGPT to a workspace.