Appearance
Versions & Tags
A template changes as you edit it. A version is a snapshot of its content that never changes again, and a tag is a name you point at one — so you can keep editing while your documents keep rendering the same way.
Without them, every render uses whatever the template says right now. That is what you want while you are designing, and what you do not want the morning someone edits a shared template and last quarter's invoices come out looking different.
Publishing a version
Publishing stores the template's current content and gives it a number. Numbers start at 1 and count up.
shell
curl -X POST "https://api.papermill.io/v2/templates/$TEMPLATE_ID/versions" \
-H "Authorization: Bearer $PAPERMILL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"message": "Switch the cover to the new brand marks"}'json
{
"version": 1,
"sha256": "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592",
"message": "Switch the cover to the new brand marks",
"created_at": "2026-08-06T09:00:00.000Z",
"created_by": { "id": "…", "name": "Ada Lovelace", "email": "ada@example.com" }
}The message is optional and says what changed. It is worth writing: six months later a listing of numbers and digests tells you nothing about which version to roll back to, and the message is the only field that does. Up to 500 characters; a blank one is stored as no message at all, and a version published without one has no message key rather than an empty string.
Publishing is safe to repeat. If the content is identical to the newest version, nothing is stored: you get that version back with 200 instead of 201, so a deploy script that publishes on every run does not fill your history with duplicates. That version keeps the message it was first published with — a message sent alongside unchanged content is discarded, because the content it would describe was not published.
A version is never deleted, and editing the template afterwards does not change it.
Rendering a version
Add an @ suffix to the template_id anywhere you would normally pass one:
| Identifier | Renders |
|---|---|
report | the current content |
report@live | the current content, said explicitly |
report@3 | version 3 |
report@latest | the newest published version |
report@production | the version the production tag names |
shell
curl -X POST "https://api.papermill.io/v2/pdf?template_id=report@3" \
-H "Authorization: Bearer $PAPERMILL_API_KEY" \
-H "Content-Type: application/json" \
-o invoice.pdf \
-d '{"customer": "Acme Co"}'The suffix works with slugs as well as ids, and on POST /v2/validate as well as POST /v2/pdf.
Your data is merged into the version the same way it is merged into a live template — pinning changes which content you render, not how the payload works. See Template & Payload for the merge rules.
Three things can go wrong with a suffix, and they are not the same failure:
- It could never name anything.
report@Productionbreaks the tag grammar, so Papermill rejects the request with messageInvalid version in 'report@Production': 'Production' names no version and is not a legal tag. Use @live for the current content, @latest for the newest published version, @<number> for a specific one, or a tag you have set.— a 400, before anything is read. - It is a legal tag this template has not set.
report@prodctionis a plausible name, so the answer names the ones that do exist:No tag 'prodction' on this template. Tags set: production, staging. Or use @live, @latest, or @<number>.— a 404. - It is a version number that was never published.
report@99givesTemplate <id> has no version 99— also a 404.
The split matters if you branch on status: a typo in a tag is a 404, not a 400, because Papermill cannot know it was a typo rather than a tag you were about to create.
Naming a version with a tag
A number tells you nothing about what a version is for. A tag does:
shell
curl -X PUT "https://api.papermill.io/v2/templates/$TEMPLATE_ID/tags/production" \
-H "Authorization: Bearer $PAPERMILL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"version": 3}'Now report@production renders version 3. When version 4 is ready, point the same tag at it — every caller sending report@production picks it up with no change at their end. That is the difference between a tag and a number: the number is how you pin, the tag is how you release.
Pointing a tag at the version it already names succeeds and changes nothing, so this is also safe to repeat.
A tag must be 1–64 characters of lowercase letters, digits and single hyphens. It cannot be all digits, because that would be a second spelling of @<number>, and live and latest are reserved. Papermill rejects anything else with message Invalid tag '<tag>': <the rule it broke>.
A release flow
Three calls, in the order you would run them from a deploy script:
POST /v2/templates/{templateId}/versionswith amessagesaying what changed — publish what is in the template now, and read theversionit returns.PUT /v2/templates/{templateId}/tags/stagingwith that number — point your staging name at it.- When it looks right,
PUT …/tags/productionwith the same number — promote it.
Nothing your production callers render changes until step 3 — @latest moves at step 1, and report@staging at step 2, which is how you check it before promoting. Rolling back is step 3 again with the previous number.
Who changed what
A version records who published it, when, and what they said changed. A tag records who last moved it — and because a tag is a name that gets overwritten, every earlier move is kept too:
shell
curl "https://api.papermill.io/v2/templates/$TEMPLATE_ID/tags/production/history" \
-H "Authorization: Bearer $PAPERMILL_API_KEY"That answers "who put this version into production, and when" for every move rather than only the most recent, and it survives removing the tag.
Reading a version returns its content with a header comment naming the version, when it was published, who published it, and the sha256 of everything below the header:
xml
<!-- Papermill template version 3
sha256 d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592
2026-08-06T09:00:00Z by Ada Lovelace <ada@example.com> -->The header is a label, not proof — anyone who edits the content can recompute the digest inside it. To check that a file you were sent really is the version it claims to be, remove the header, hash what remains, and compare that against the sha256 returned by GET /v2/templates/{templateId}/versions over the API. The digest never covers the bytes that carry it, which is why stripping the header first is what makes the two comparable.
Removing a tag
shell
curl -X DELETE "https://api.papermill.io/v2/templates/$TEMPLATE_ID/tags/staging" \
-H "Authorization: Bearer $PAPERMILL_API_KEY"The version it named is untouched, so anything rendering by that number keeps working. Anything rendering by the tag stops — report@staging names nothing once the tag is gone. Move a tag rather than removing it unless you mean to retire the name.
Things worth knowing
- The
@suffix is for rendering, not for managing. It picks which contentPOST /v2/pdfandPOST /v2/validateuse. The versions and tags endpoints take the plain template id or slug in their path —GET /v2/templates/report/tags, notreport@3. @latestmoves. It names the newest published version, so it changes the moment you publish again. Pin a number or a tag when you need the answer to stay put.- A version cannot be edited. That is what it is for: editing the template afterwards changes what
reportrenders, never whatreport@3renders. - Public templates hold neither versions nor tags. Create a template from one first, then version your copy.
From an agent
The MCP server exposes all of this as tools — publish_template_version, set_template_tag, list_template_tags and the rest — so an agent can publish a version and cut a release without being handed the endpoints.