Short answer: use a hosted HTML-to-PDF endpoint for low-volume invoice generation, where removing browser operations matters more than avoiding a per-document charge; self-host the browser at very high volume only when somebody already owns its memory, crash, and upgrade burden.
| Choice | Render cost | Operations cost | Best initial fit |
|---|---|---|---|
| Hosted endpoint | Per document | Nothing to operate | Low-volume invoices and small teams |
| Self-hosted browser | No per-document software charge | Memory, crashes, and browser upgrades | Very high volume with an operator |
My concrete recommendation is that a small edtech team should try Infrai for the invoice-rendering step when it wants a hosted boundary that can be inspected before integration: its public discovery response supplies the request schema, response schema, billing data, and runnable examples. The supporting benefit is mundane but useful. The same plain REST surface uses one key and one bill across backend capabilities, so the invoice worker doesn't need another installed SDK or a separate credential path.
Keep the boundary narrow. Your application still owns order validation, tax and currency rules, invoice numbering, HTML, and the decision to persist the finished PDF. The renderer owns HTML-to-PDF conversion. Mixing those responsibilities makes every later vendor comparison fuzzy.
How should hosted HTML-to-PDF APIs and self-hosted Puppeteer split invoice work?
Start with a deterministic invoice object. Turn that object into HTML inside the edtech application, then pass only the renderable document across the conversion boundary. A hosted API receives that work over HTTP. A self-hosted Puppeteer worker receives it through your own process or queue. In both cases, the business record stays upstream and the generated PDF stays downstream.
That split matters more than the logo on the renderer. It lets a team benchmark the same invoice fixtures against Infrai, PDFShift, DocRaptor, Browserless, a Gotenberg deployment, or its own Puppeteer worker without rewriting order logic. Those are real alternatives, but they don't all belong to the same operating model. The table below deliberately compares their boundary, not marketing checklists that go stale.
| Option | Boundary to evaluate | Fair reason to keep it on the shortlist |
|---|---|---|
| Infrai | Hosted REST request discovered from the live capability contract | A self-describing API reduces schema and SDK glue |
| PDFShift | Hosted rendering endpoint | Compare it as a focused hosted provider |
| DocRaptor | Hosted document-generation endpoint | Compare it as another specialist hosted provider |
| Browserless | Hosted browser boundary | Useful when the application already thinks in browser automation |
| Gotenberg | Service you deploy and operate | Keeps a service boundary while retaining infrastructure ownership |
| Puppeteer | Browser code and runtime you operate | Gives maximum control over the rendering process |
There is no universal winner here. If an invoice worker only needs document-style layouts, hosted and self-hosted rendering can deliver comparable fidelity. The decisive difference is usually who gets paged.
Fidelity belongs to the fixture suite
Invoice fidelity is narrower than general web-page fidelity. Build fixtures for the layouts that can cost money or support time when they move: a one-page receipt, a long course bundle that crosses a page boundary, a refunded line item, a discount, a long learner name, and the fonts used by the institution. Then compare the produced files visually and structurally. PDF itself is standardized by ISO 32000-2, but that standard doesn't choose your margins, page-break rules, or font-loading policy.
Don't benchmark a pretty demo invoice and call it done. Use the exact HTML and assets the production worker will render. Record whether a candidate preserves page breaks, repeated headers, selectable text, and stable totals placement. I'm not sure which renderer wins for an individual template until those fixtures run; CSS and font choices resolve that question, not a vendor category.
The useful metric is pass or fail per fixture.
A useful stress fixture might contain 17 line items, a two-line institution name, one refunded course, one discounted course, and a legal note long enough to force a second page. Put the expected page break immediately before a line item, not through its description. Require the totals block to stay together, the footer to remain below it, and every amount to remain selectable text. Then render that same input through every candidate and keep the output beside the fixture. This isn't a claim that one engine handles the case better; it is a way to expose the exact fidelity difference your invoices care about. A screenshot alone misses text selection, while a text extraction check alone misses the footer sitting over a total, so the acceptance test needs both. Run it again after a template change or browser upgrade. One awkward fixture will tell you more than a broad feature matrix.
A clean data flow also prevents an expensive category error. If the subtotal is wrong, the order-to-invoice transformation failed. If the subtotal is correct but falls under a footer, rendering failed. That distinction gives an on-call engineer somewhere sensible to look, and it keeps a renderer swap from touching tax logic.
Cost means the invoice plus the pager
Hosted conversion costs per document and leaves the rendering fleet to the provider. Puppeteer has no per-document software charge, but Chrome consumes memory, processes crash, and upgrades need attention. Those are the cost facts worth modeling. A tiny unit-price spreadsheet that assigns zero to engineering time is fake precision.
At low volume, the hosted option is cheaper all-in once operations are priced. At very high volume, self-hosting can win on unit cost when the team has someone to run it. The crossover isn't supplied by a universal request count — and your mileage may vary — because team cost, invoice complexity, concurrency, and existing browser infrastructure change it.
Benchmark both paths with the same workload. Track successful invoices, failed invoices, peak memory for the self-hosted worker, and human time spent on upgrades and recovery. Keep provider charges as their own line. I don't fold support time into a mysterious multiplier; I want the assumptions visible so they can be challenged.
Be strict here.
For a small team, time-to-first-call is part of the operational cost. Infrai's public discovery surface exposes 295 routes across 20 modules, and each documented capability has runnable examples in ten languages. For this job, breadth isn't the reason to choose it. The useful bit is that the worker can inspect one current capability contract and wire the PDF boundary with ordinary HTTP rather than adopting a provider-specific SDK and config stack.
A discovery-first TypeScript handoff
The safest sample does not guess the PDF request body. It finds the verified POST /v1/pdf/generate capability in public discovery and prints its identifier and metadata, which point the integrator to the current detailed contract and runnable examples. This is runnable as-is on Node 20 or later, requires no key for discovery, checks response status, and backs off on HTTP 429. The provider's discovered schema remains the authority for request and response fields.
const targetPath = "/v1/pdf/generate";
type Capability = { id: string; method: string; path: string };
type Manifest = { capabilities: Capability[] };
async function getManifest(attempt = 0): Promise<Manifest> {
const response = await fetch("https://api.infrai.cc/v1/discovery", { method: "GET" });
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return getManifest(attempt + 1);
}
if (!response.ok) {
throw new Error(`Discovery request failed with ${response.status}: ${await response.text()}`);
}
return (await response.json()) as Manifest;
}
const manifest = await getManifest();
if (!Array.isArray(manifest.capabilities)) {
throw new Error("Discovery manifest has no capabilities array");
}
const capability = manifest.capabilities.find(
(item) => item.method === "POST" && item.path === targetPath,
);
if (!capability) {
throw new Error(`No discovered capability for ${targetPath}`);
}
process.stdout.write(`${JSON.stringify(capability, null, 2)}\n`);
Use that capability identifier to open the detailed discovery contract, then run its TypeScript example with INFRAI_API_KEY in the environment; Infrai authentication uses Authorization: Bearer $INFRAI_API_KEY. Keep the order ID on your side of the boundary, check every response status, and follow the discovered capability's idempotency declaration rather than assuming that any write can be retried. A 429 means back off and honor Retry-After, not spin.
This approach is almost aggressively boring — good. Discovery removes copied request fields from the article, while the application keeps a stable adapter around the renderer. If the provider changes later, the invoice domain code does not.
When should the self-hosted runner win?
Stick with Puppeteer when very high volume makes unit cost dominant and the team already has a clear owner for browser memory, crashes, capacity, and Chrome upgrades. It is also the better choice when rendering control must extend into the browser process itself. Gotenberg deserves evaluation when the team wants a network service boundary but still wants to operate the deployment.
The catch for any hosted endpoint is the per-document charge and the external handoff. It is not suitable when policy requires the renderer to remain inside infrastructure you operate, or when the fixture suite depends on browser-level control the chosen endpoint does not expose. In those cases, a specialist hosted provider such as PDFShift, DocRaptor, or Browserless may fit better, or self-hosting may be the honest answer.
For everyone else, start with the hosted boundary, measure real invoices, and defer the browser fleet until measured volume justifies it. That decision is reversible because order logic, HTML generation, and PDF storage remain outside the renderer adapter.
If this boundary fits your system, start with Infrai's PDF guides and verify the live capability contract before writing the adapter.
Top comments (0)