Three ways to generate an invoice PDF, and when each is wrong
Including the case where you should not use an API at all. Written by people who sell one, so read the "when to build it yourself" section first.
There are three real ways to turn structured data into an invoice PDF. Each is the right answer somewhere and a bad answer somewhere else, and the deciding factors are rarely the ones in a feature table.
We sell one of the three. So this page leads with the cases where you should not buy it.
Approach 1 — render HTML, print it with a headless browser
Build the invoice as an HTML page, run Chromium via Puppeteer or Playwright, call
page.pdf(). Historically wkhtmltopdf, now largely superseded.
Where it wins
- Design iteration is instant. Your designer edits CSS; nobody writes drawing code. For a document that changes often, or that must match a marketing-designed template exactly, nothing else is close.
- You reuse the web stack you already have — components, i18n, theming.
- Complex layout (flexbox, grid, web fonts, SVG logos) works because a real browser is doing it.
Where it hurts
- Chromium is ~300 MB and wants ~1 GB of RAM. On serverless this is the whole problem: cold starts run into seconds, and you fight layer size limits. On a container it is fine but it is now the largest thing in your image.
- Non-deterministic output. A Chromium upgrade can shift text by a pixel or re-break a line. Usually invisible; occasionally it pushes your total onto page two.
- Font availability differs between your laptop and the container. Missing font, silent fallback, different metrics — and accented Portuguese characters are exactly where a fallback font shows.
- It is a browser: a security surface that renders arbitrary content. If any input reaches the HTML unescaped, you now have SSRF and local-file-read to think about.
"Just run Puppeteer in a Lambda" is where a lot of teams lose a week. It is doable with
@sparticuz/chromium and friends, but you are managing a stripped Chromium build, cold
starts around 2–5 s, and memory tuning — for a document that takes milliseconds to actually lay
out.
Approach 2 — draw it with a PDF library
Position text and lines directly: pdf-lib or PDFKit in JS, ReportLab in Python, FPDF or
TCPDF in PHP, iText in Java, gofpdf in Go.
Where it wins
- Tiny and fast. No browser. Milliseconds per document, megabytes of dependency, trivial cold start. This is what makes serverless invoice generation pleasant.
- Deterministic. The same input produces byte-comparable output, which means you can snapshot-test it and cache by payload hash.
- Full control: exact positioning, embedded fonts you chose, no surprise reflow.
- No security surface beyond your own code.
Where it hurts
- You write layout code. Column widths, page breaks, "does this description wrap onto a second line and push everything down". Table pagination with a repeating header is a genuinely fiddly afternoon.
- Design changes are code changes. A designer cannot touch it.
- Fonts must be embedded, and the standard PDF base-14 fonts use WinAnsi encoding — fine for Portuguese accents, but not for anything outside Latin-1, where you need a real embedded font.
Including this one: FaturaPDF draws with pdf-lib. There is no browser in the request path,
which is why median render time is ~34 ms with a PIX QR and ~3 ms without (measured over 200 local
runs of the same code path, excluding network). If you build it yourself with a library, you get the
same performance profile — you just also get the layout work.
Approach 3 — call a document API
POST JSON, receive PDF. Someone else owns the layout, the fonts and the edge cases.
Where it wins
- The layout is a solved problem you did not want to solve. Especially when it is domain-specific: Brazilian fiscal fields, check-digit validation, currency in words, a PIX QR in the right place.
- Nothing to deploy, no fonts to ship, no cold start of your own.
- Domain rules stay current without your involvement.
Where it hurts — read this part
- A network dependency in a path that used to be local. Now you need timeouts, retries and a story for "the API is down at month end".
- Data leaves your system. Customer names and tax IDs go to a third party. Under the LGPD that is a processing decision requiring a legal basis — not a blocker, but not nothing.
- Per-document cost. Fine at 500/month; do the arithmetic at 500,000.
- Limited customization. If you need your exact brand template, an API with a fixed layout is the wrong tool and no amount of parameters fixes it.
- Vendor risk. A small API can disappear. Keep the payload construction behind your own interface so swapping it is a day, not a quarter.
- You generate at high volume and the per-document cost exceeds the engineering cost of a library. Run the numbers; the crossover is lower than vendors imply.
- Your document must match a specific brand template with your own typography.
- You cannot send customer data to a third party, for policy or regulatory reasons.
- You already have a working PDF pipeline. Replacing something that works is rarely the highest-value thing on your board.
Side by side
| Headless browser | PDF library | Document API | |
|---|---|---|---|
| Time to first working PDF | Hours | Days | Minutes |
| Time to a good invoice | Days | 1–2 weeks | Minutes |
| Dependency size | ~300 MB | ~1–5 MB | Zero |
| Cold start | Seconds | Milliseconds | Your HTTP client only |
| Per-document latency | 200 ms – 2 s | 1–50 ms | Network + render |
| Deterministic output | No | Yes | Yes, for a fixed payload |
| Design iteration | Excellent (CSS) | Poor (code) | None (fixed layout) |
| Marginal cost | Compute | ~Zero | Per document |
| Data leaves your system | No | No | Yes |
| Ongoing maintenance | Chromium upgrades | Layout edge cases | None |
How to actually choose
- Is the document a core product surface your designers own? → Headless browser. Accept the ops cost; design velocity is worth more.
- Is volume high, layout stable, and data sensitive? → PDF library. Pay the two weeks once.
- Is it a supporting feature with domain-specific formatting you do not want to learn? → API. This is the Brazilian-invoice case: CPF/CNPJ check digits, BRL, DD/MM/YYYY, amount in words, PIX QR placement — a fortnight of learning for something that is not your product.
- Not sure? Start with the API, keep the payload construction behind your own interface, and measure. If volume or customization pressure grows past it, you swap the implementation of one function.
// Whatever you choose, put it behind this. Swapping approaches then costs a day.
export interface InvoiceRenderer {
render(doc: InvoiceDocument): Promise<Uint8Array>;
}
export class ApiRenderer implements InvoiceRenderer { /* HTTP POST */ }
export class LocalRenderer implements InvoiceRenderer { /* pdf-lib */ }
// Composition root decides. Tests use a stub. Nothing else knows.
const renderer: InvoiceRenderer = process.env.INVOICE_RENDERER === "local"
? new LocalRenderer()
: new ApiRenderer(process.env.RAPIDAPI_KEY!);This also gives you a free migration path and a free incident response: if the API is down at month end, flipping an environment variable to a degraded local renderer beats a queue of failed invoices.
The Brazil-specific wrinkle
One factor genuinely tilts the decision for Brazilian documents: the domain rules are not obvious from outside, and each one is a small bug waiting to happen. Check-digit validation. Thousands-dot currency. The float rounding that turns R$ 1,005.00 into the wrong centavo. Amount in words with the cem/cento and "de reais" rules. DD/MM/YYYY without a timezone shift. PIX BR Code with its byte-counted TLV fields.
None is hard. Together they are the fortnight. If you are building a Brazilian billing product, that fortnight is your core competence and you should own it. If Brazil is one market among many in a product about something else, it probably is not.
Either way, the rules themselves are documented here — free, and independent of whether you ever call the API: check digits, currency, dates and amounts in words, the PIX BR Code format.
Frequently asked questions
Is Puppeteer really that bad for PDF generation?
Not bad — expensive. It is a great fit when design iteration matters and you run containers with headroom. It is a poor fit on serverless, where a ~300 MB dependency and multi-second cold starts dominate the milliseconds of actual work.
At what volume does building it yourself win?
Compare the monthly API bill against roughly one to two engineer-weeks of build plus ongoing maintenance. Below a few thousand documents a month the API almost always wins; above tens of thousands the library usually does. In between, the deciding factor is normally customization, not cost.
Can I switch later without a rewrite?
Yes, if you put a render(doc): Promise<bytes> interface between your domain and the implementation from day one. That is roughly ten lines of discipline that turn a migration into an afternoon.
What does FaturaPDF use internally?
Approach 2 — pdf-lib, pure JavaScript, no browser in the request path. That is why it runs on a small serverless function with millisecond render times. If you take the same approach yourself you get the same performance; the difference is who writes the layout and the Brazilian formatting rules.
If the API is the right answer for you
FaturaPDF is the narrow case: Brazilian invoice and receipt layouts, validated CPF/CNPJ, BRL and PIX, one HTTP call. Free tier of 20 documents/month, no card, so you can decide with evidence rather than a pricing page.
Get an API key on RapidAPI → Or try the free browser generator