Skip to main content

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 write scope for anything that changes a version. Reading version history needs only read.
  • 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_number is old_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:

PlanRevert window
Free, Pro7 days from the version's creation
Team, Business, EnterpriseUnlimited

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_versions returns thumbnails: [] 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:

CodeStatusMeaning
versioning_not_enabled403The workspace plan does not allow version writes.
version_write_forbidden403The 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_exceeded403Target is older than the plan's revert window. Also carries revert_window_days.
version_not_found404No such version_number — above the head, or a gap.
invalid_version_number422Not a positive integer.
version_blob_missing422The 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:

ToolWhat it does
list_asset_versionsVersion history, newest first, plus head_version_number. Read-only.
revert_assetRevert to a version_number. Needs write.
create_asset_versionUpload a public URL as a new version of an existing asset. Needs write.
edit_asset_version_commentEdit 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.