DocgenTry it

Frozen versions

Templates are mutable — you edit them, save them, iterate. Renders aren't. When you submit a render, you pin a version, and the version's body is immutable forever.

Why versions exist

Imagine an invoice template that ships in June. Twenty invoices render against it. In July, you tweak the layout — change the margin, swap a logo, fix a typo. Without versions, the next time someone re-renders June's PDFs (because their email got eaten, or for an audit), they get the July layout. That's wrong. Renders should always reproduce exactly what shipped.

Versions solve this by snapshotting the template body at freeze time. Once a version exists, its body_snapshot never changes — not even if you delete the template, not even if you edit the draft. Reproducibility by construction.

Lifecycle

POST /v1/templates           ← creates a draft (mutable)
PATCH /v1/templates/{id}     ← edits the draft
POST /v1/templates/{id}/versions   ← freezes the current draft as v1
                                     (or v2, v3, … — auto-incremented)
POST /v1/renders             ← submits a render against a specific version

What gets frozen

A version captures three things, atomically:

  • The body_snapshot — the full template source at freeze time.
  • The fields_schema — the merge-field schema discovered from that body. So even if the discovery logic changes later, the schema reported at freeze time is what renders validate against.
  • The label — auto-incremented (v1, v2, …).

Choosing a version at render time

The render request takes an optional version field:

POST /v1/renders
{
  "template_id": "01HX...",
  "version": "v3",       // optional; defaults to the latest version
  "formats": ["pdf"],
  "data": { ... }
}

If you omit version, the API uses the latest. If you pass an explicit label that doesn't exist, you get a 404.

What if the template has no versions?

Renders refuse to run against an unversioned template:

{
  "type": "about:blank",
  "title": "Unprocessable",
  "status": 422,
  "detail": "Template has no frozen versions. Freeze a version with POST /v1/templates/{id}/versions before rendering."
}

This is deliberate. Renders against an in-progress draft are how documents end up looking different than expected six months later. Freezing is one HTTP call; do it once before you go live.

Versioning strategy in practice

  • Freeze before deploy.Lock the template version in CI right before your code ships, so the deployed code and the template version that's live both bear the same release tag.
  • Pin the version in your code. Store template_version: "v3" in your config, not in your database. Bumps are deploys, not data migrations.
  • Keep the draft livable. Edit freely between freezes. The draft is your sandbox; the version is the contract.

Next

Async job lifecycle — how renders queue, run, and resolve.