Comparison

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.

Updated 2026-08-07 · 9 min read · by the FaturaPDF team

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.
The serverless trap

"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.
This is what most invoice APIs are, underneath

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.
Do not use an API if…
  • 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 browserPDF libraryDocument API
Time to first working PDFHoursDaysMinutes
Time to a good invoiceDays1–2 weeksMinutes
Dependency size~300 MB~1–5 MBZero
Cold startSecondsMillisecondsYour HTTP client only
Per-document latency200 ms – 2 s1–50 msNetwork + render
Deterministic outputNoYesYes, for a fixed payload
Design iterationExcellent (CSS)Poor (code)None (fixed layout)
Marginal costCompute~ZeroPer document
Data leaves your systemNoNoYes
Ongoing maintenanceChromium upgradesLayout edge casesNone

How to actually choose

  1. Is the document a core product surface your designers own? → Headless browser. Accept the ops cost; design velocity is worth more.
  2. Is volume high, layout stable, and data sensitive? → PDF library. Pay the two weeks once.
  3. 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.
  4. 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.
typescriptrenderer.ts
// 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

Related guides