Most advice about generating documents from a web app assumes you control what the document looks like. You pick the layout, your designer argues about the logo, and the template count is however many document types you ship. Three, maybe five.
That advice is useless if your documents are read by a court clerk.
There is a whole class of software where the format is set by the recipient and not by you: process-service platforms filing affidavits, inspection software whose reports go to an authority having jurisdiction, scale operators issuing tickets under state scrap-metal reporting rules, construction software producing change orders on whatever form the general contractor insists on, veterinary software issuing interstate health certificates, funeral-home software producing the death certificate, the burial transit permit and the cremation authorization that each state prescribes for itself, impound software mailing statutory lien notices to a registered owner. In all of them the same facts have to come out in somebody else's shape, and which shape depends on where the job happened.
That is a different engineering problem from branding, and it gets treated as the same one far too often. Worth separating them properly.
Why it isn't the branding problem
If the variation is branding, the count is bounded by your customer list and the variation is shallow. Logo, two or three colours, a footer. You can do all of it with one layout and some values, and most template systems handle it without anybody thinking hard.
When the recipient sets the format, three things change.
The count is set by a list you don't own. Fifty states, or however many counties your customers file in, or every AHJ in a metro area. You don't get to consolidate it, because the consolidation would have to happen at the clerk's desk.
It only ever grows. Your customers expand into a new state and the new state has its own form. Nobody is going to come along and reduce the number of jurisdictions. Compare that with branding, where churn at least gives some back.
And it can grow without any customer asking. In the regulated-document verticals there is a second counter nobody mentions: approval. A vendor is approved to issue the document in some jurisdictions and not in others, which you can watch happening on their own marketing pages — one will say it is an approved provider in thirty-nine states "and adding more", with a map splitting those into the ones where it can do the search and the notification and the ones where it can only do the search; another says it is approved in all fifty. Each new approval is a new form, and it arrives on the regulator's timetable rather than on a customer's. The same pages tend to publish a per-jurisdiction mail method alongside the fee, which means the thing coming out of the printer has to satisfy two authorities at once: the one that sets the form and the one that carries it.
Two consequences for how you build this. The first is that the count is a rate rather than a number, so an architecture that is fine at nine forms and painful at forty is the wrong architecture even while you have nine. The second is that somebody outside engineering learns about each new form first — whoever tracks the approvals — and the shorter the path from them to a rendered document, the less often a layout change needs a release.
The differences are real, not cosmetic. Different required fields, different field order, different declarations at the bottom, sometimes a different number of pages. "Same layout, different CSS" does not survive contact with two counties' forms side by side. Occasionally two documents in one jurisdiction are one document in the next, which no amount of conditional CSS will reach.
And some of the requirements are layout requirements, which is the part that surprises people who have only done branding: a prescribed field order, a page that has to exist even when it is empty, a signature block in a fixed place. None of those is a value in your data, and none of them is something a validator on your form would ever catch.
That is the general shape of this problem and it is why it deserves its own architecture. When somebody else picks the format, presentation becomes correctness. The gap between "the data is right" and "the document is right" stops being a design question.
What makes the problem tractable is the thing that makes it annoying: these layouts are stable. A county form changes when the county amends it, which is rarely and with notice. So you have a large and slowly-growing set of fixed shapes, each taking variable data. That is a good position to be in. It is nothing like the arbitrary-document problem, and it should not be solved with the same tools.
The four ways to do this, and one of them is a trap
0. The free-text box, which is what most products actually do
Before the three real options, the one you have probably already built, because almost everybody in this category has.
You keep a single layout for the document, and you add a settings field where the customer can type extra wording that gets printed on it. It is usually called something like notes and disclaimers. The vendor documentation I read while writing this describes its purpose as "standard disclaimers, clinic policies, or state-required language", and that third item is the tell: a free-text box is being used to carry a statutory difference between jurisdictions.
It is easy to see why it happens. It ships in an afternoon, it needs no new architecture, and it genuinely does solve the first two or three cases. It also quietly moves the compliance problem onto your customer, which is why it is worth naming rather than leaving as the silent default.
What it costs:
- The customer is now the one who has to know the law, per state, and keep up when it changes. They typed the wording in once, in a settings screen, probably years ago.
- You cannot see whether it is right. The text is opaque to you. No validation, no versioning you can reason about, no way to push a correction when a state amends its wording.
- It only reaches wording. A different field order, an extra page, a different signature block — none of those are reachable from a text box, so the cases it cannot solve are the cases that invalidate the document.
- Per-customer, not per-jurisdiction. A clinic operating in two states has one field, and the number of jurisdictions it is trying to represent is two.
If you have this and it is working, that is fine and you do not need to tear it out; for clinic policy text it is exactly the right tool. The point is only that it is not a jurisdiction strategy, and the moment the second state's form differs in anything but wording you need one of the three below.
1. Fill the official PDF
If the jurisdiction publishes a fillable PDF — an AcroForm with named
fields — then fill that. Don't redraw it.
pdf-lib, PDFtk, iText, pick one; you load their file, set
the field values, flatten it, done.
This is the right answer more often than people expect and I would start here every time. The output is byte-for-byte the form the clerk is expecting, you inherit their revisions by downloading the new file, and you never have to defend a pixel. The work is in maintaining a field map per form, which is dull but honest.
Where it falls apart: a lot of authorities publish a scan, or a Word file, or a PDF with no form fields at all. Then there is nothing to fill and you are drawing it yourself anyway.
2. Draw it yourself with a converter
HTML and CSS per form, run through a headless browser or a print engine. Puppeteer, Playwright, Gotenberg, WeasyPrint, wkhtmltopdf if you are still on it.
This is the default and it works. Two costs worth naming before you commit. Running a browser pool in production is its own operational job — memory, zombie processes, concurrency at month-end when everybody files at once. And the templates live in your repo, which means adding Clark County is a deploy.
That second one matters more than it sounds when the count is in the dozens and growing. Every new jurisdiction becomes a pull request, a review, a release. Teams in this position usually end up building an admin screen so that someone who isn't an engineer can add a form, and at that point they have written half a template service.
3. Hosted templates, called by id
Keep the layouts outside your codebase, author them where they can be edited without a deploy, and call them by name at render time.
This is what iReceipt PRO does, and it is the part of the design that actually bears on this problem, so here it is concretely. The template id sits in the URL path:
POST https://api.ireceipt.pro/v1/pdf/private/{templateId}
Authorization: Bearer <api key>
{ "variables": { ... } }
A 201 comes back with the file as the body. Not a 200 — that one catches people.
Which means the code that renders a Nevada affidavit and the code that renders a Texas one is the same code with a different string in it. Picking the layout is routing:
const TEMPLATE_BY_STATE = {
NV: 'affidavit_nv_...',
TX: 'affidavit_tx_...',
// and so on
}
await client.createPdfFromPrivateTemplate(TEMPLATE_BY_STATE[job.state], args)
Adding a jurisdiction is a new template and a new line in that map. Not a rewrite, and not a browser pool.
One naming wrinkle, since it trips people on the first call: the SDK
argument is called args and the wire format calls the same
thing variables. There are eight methods, one per output
format and scope —
createPdfFromPrivateTemplate through
createWebpFromPublicTemplate — and they all take
(templateId, args, size?).
Templates are HTML underneath with Mustache-style substitution, so
{{ }} tags take whatever you hand them, including a hex
colour or a base64 image. The rendering side holds the layout; your side
holds the data. There is a public sandbox that renders any public
template with no login, if you want to see the shape of it before
writing anything, at
dashboard.ireceipt.pro under
/sandbox/public/ plus a template id.
The thing a US reader will hit in the first hour
Our named page-format list is A-series only.
a1-portrait through a7-landscape, fourteen
values, both orientations, default A4. There is no
letter in it and no legal.
For this exact audience that is close to a dealbreaker on its face, because US court and municipal forms are Letter and occasionally Legal, and nobody is printing a county affidavit on A4.
The workaround is real and it is in the same spec.
size takes a pixel object whose default is 796×1126, which
is A4 at 96dpi. At that same 96dpi, Letter is
{"width": 816, "height": 1056} and Legal is
{"width": 816, "height": 1344}. Pass those with
format omitted, because
format takes priority over size and will
override it:
{ "variables": { }, "size": { "width": 816, "height": 1056 } }
Both the enum and the priority rule are checkable at api.ireceipt.pro/openapi.yml, where the size field is documented in so many words as "less priority than format". What I have not done is render at those values and measure the result with a ruler, so take the arithmetic as a starting point rather than a guarantee.
One limit that is in the code rather than the spec: on the free plan a
PDF can be at most 1200 px on either side. Letter fits; Legal, at
1344 px tall, answers 403 with
A size larger than 1200x1200 requires a paid subscription.
I would rather say this up front than have you find it on your own and conclude the product was not built for your continent.
When this is the wrong tool
Three cases, and they are common enough to be worth being blunt about.
The jurisdiction publishes a fillable PDF. Go back to
option 1. Filling their file beats redrawing it, every time, and a
template service is a worse answer than pdf-lib here.
You need to render HTML you are holding at request time.
There is no raw HTML-to-PDF endpoint. Every call names a template that
already lives on the rendering side. If your templates are in your repo
today with a render_to_string around them, moving here
means re-authoring them and inverting who holds the layout — a genuinely
good trade for a fixed set of document shapes, and a bad one if the HTML
is arbitrary. For arbitrary HTML, use a converter. Gotenberg and
WeasyPrint are both good and neither of them is us.
Your variation is really branding after all. If the count is bounded by your customer list, you probably don't need a template per customer and you certainly don't need a per-jurisdiction architecture. One layout plus values, and move on.
The limits, in one place
Stated plainly because you would find them anyway and it is quicker this way.
-
Named page formats are A-series. Letter and Legal come from the pixel
sizeobject withformatomitted, as above. - No raw HTML endpoint. Templates live on the rendering side.
- We don't publish prices here; they are in the dashboard behind a login. The quota mechanics are public in the FAQ, the numbers are not.
- Rendered content isn't stored — the published privacy policy says "we do not collect or store information received by the API during the use of the service". Read the rest of that document too if it matters to you: Georgia governing law, a liability cap of $100 that is further limited to half of your current monthly subscription, and there is no DPA on offer today. For a regulated buyer those are material, and quoting only the first sentence would be misleading.
- No published uptime record. There is a status page, but it is a live check from your own browser with no history, so don't read it as an SLA.
- No SOC 2, no ISO 27001, no data-residency choice.