Here is a bug that is genuinely hard to find. A contract you generated in March renders differently in September. Same template, same data, different document. Nothing in your code changed.
What changed is that somebody moved a box in the editor in June.
Drafts and versions are different things
A template in Crixaa has two kinds of state, and the distinction is the whole point.
A draft is what you see in the editor. It saves continuously as you work, it is shared live with anyone else editing, and it is expected to be half-finished. That is what a draft is for.
A version is a committed snapshot. It does not change. When you commit, the current draft is frozen and given a number, and that number will render the same document in five years as it does today.
Generation always runs against a version, never a draft. This is the property that makes a generated document defensible: you can point at the exact version that produced it.
What that means for the API
POST /api/v1/templates/{id}/generate takes an optional versionId.
If you pass one, you get that version. Deterministically, forever.
{
"versionId": "3f2a…",
"data": { "customer": "Acme Ltd", "total": 1200 }
}
If you omit it, we use the latest committed version at the moment of the call. Which is convenient — and is exactly how the September document ended up different from the March one. Somebody committed in June, and every call since has been picking up the new one.
Neither behaviour is wrong. They answer different questions:
- Omit
versionIdwhen you want the current template. Marketing collateral, an internal report, anything where "latest" is what you actually mean. - Pin
versionIdwhen the document is a record. Contracts, invoices, anything a customer or an auditor might come back to.
The rule of thumb: if you would be uncomfortable explaining why a regenerated document does not match the original, pin the version.
Pin it by storing it
Pinning only helps if you keep the id. Store the versionId alongside whatever record the
document belongs to:
ALTER TABLE invoices ADD COLUMN template_version_id UUID;
Now regenerating an invoice from 2026 uses the 2026 template, and "why does this look
different?" stops being a question anyone has to investigate. You can enumerate the
available ids from GET /api/v1/templates/{id}/versions whenever you need to.
A template with no versions cannot generate
If you call generate on a template that has only ever been a draft, you get a 400:
Template has no committed versions yet — commit one before generating.
This trips people up in exactly one place: a template created in the console, styled, and then wired into an integration without anyone pressing commit. The editor looks finished, so it is a surprising error.
It is deliberately a hard failure rather than a silent fallback to the draft. Generating from a draft would mean a document produced from a state that no longer exists and cannot be recovered — the thing versions exist to prevent.
Committing is a good place for review
A commit is a natural checkpoint, and on Pro you can require approval before one lands. If your documents carry legal or financial weight, that is where the review belongs — not on the draft, which changes by the minute, and not on the generated PDF, which is far too late to be arguing about wording.
The commit is the moment the template stops being someone's work in progress and becomes the thing your systems will run thousands of times.
The short version
Draft for working, version for generating. Pin the version on anything that is a record, and store the id you pinned. It costs one column and removes a category of bug that is miserable to debug six months later, when nobody remembers moving the box.