Short answer: use an explicit, asynchronous PDF job with strict input validation, then measure fidelity and latency on your own report samples before you pick a provider. For a US/EU SaaS, keep credentials on the server, hand clients short-lived object-storage links, and decide retention and idempotency up front. The least complex option is usually a managed renderer; a self-hosted renderer wins when data residency and deep layout control outweigh the operations bill.
The PDF endpoint is only one piece of the decision. A report pipeline also has to survive retries, page-limit surprises, font differences, and a customer asking where their document went 30 days later. I judge these systems by time-to-first-call and by how much glue code they force into a CLI or worker. I don't trust a green demo until it has survived a 429 and an expired link.
The number that matters is p95.
Measure twice.
A choice matrix for report generation
| Option | Fidelity control | Latency shape | Operational load | US/EU privacy posture |
|---|---|---|---|---|
| Managed HTML-to-PDF API | Good for stable HTML/CSS; vendor controls the browser | Usually predictable for queued jobs, but measure queue time | Low | Depends on region, retention controls, and contract |
| Template/document API | Strong template governance; less freedom for arbitrary layouts | Predictable for bounded templates | Low to medium | Check where source files and outputs are stored |
| Self-hosted Chromium or WeasyPrint | Highest control over fonts, network access, and versions | You own cold starts, queues, and capacity | High | Best control, provided logs, disks, and backups are also covered |
| Platform PDF service behind your worker | Varies by cloud and renderer | Fits an existing queue and observability stack | Medium | Can align with an existing regional account and policy |
My default for an edtech SaaS is the first row: submit a job, persist a job record, and poll or receive the completed artifact through a controlled path. That is a recommendation about workflow, not a claim that every managed API is equal. The table is deliberately boring; boring is useful when a school district is asking about retention.
Which PDF endpoints should a US/EU SaaS use for report generation?
Start with a document operation, not a vendor-shaped abstraction. A template operation should have a template contract. A generated report should have an input contract, a job identifier, an output location, and an expiry policy. Keeping those boundaries explicit makes it possible to swap renderers without rewriting billing, audit, and customer notification code.
For a provider with a discovery surface, the relevant shape is clear: create a template with POST /v1/pdf/template/create, submit work with POST /v1/pdf/generate, and retrieve state with GET /v1/pdf/job/get/{job_id}. Those are different responsibilities. Do not make the browser call the generation route, and do not treat a storage URL as a permanent document ID.
The worker should validate page count estimates, input size, allowed fonts, and external resource policy before submission. Rejecting a report before it reaches a renderer is cheaper than debugging a missing chart after a customer has downloaded it. Keep a deterministic idempotency key derived from the report revision and tenant; if the queue retries, the same logical report must not create two billable artifacts.
Here is the small TypeScript client I would put behind the worker. It leaves the document schema to your validated payload, uses an explicit method, and treats a rate limit as a scheduling signal rather than a reason to spin.
const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");
async function submitPdf(payload: unknown, idempotencyKey: string) {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch(`${baseUrl}/pdf/generate`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(payload),
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("Retry-After"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1000
: 2 ** attempt * 250;
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
if (!response.ok) {
throw new Error(`PDF submission failed (${response.status}): ${await response.text()}`);
}
return response.json();
}
throw new Error("PDF submission stayed rate-limited after 5 attempts");
}
I've kept the example intentionally narrow: one write route and one retry policy. Your worker can use the returned job identifier with the documented job lookup route, while your application database remains the source of truth for tenant, revision, and retention state.
I use a small state machine: accepted, running, succeeded, failed, and expired. A separate audit record stores who requested the report, which revision was rendered, and when the output link expires. This is more useful than a generic status: done, especially when a support engineer has to distinguish an expired link from a failed render.
How should fidelity, latency, privacy, and retention shape the design?
Fidelity is not a screenshot contest. Build a representative fixture set: long tables, right-to-left text if you support it, custom fonts, charts, page breaks, and the largest real report. Compare text extraction, page count, key pixel regions, and links. I would rather have a repeatable 20-document test than a vendor demo that happens to contain one friendly invoice.
Latency needs two numbers, not one: queue wait and render time. Record both at submission and completion, then choose a service-level target that reflects the user action. A synchronous HTTP request is reasonable for a tiny preview; a final transcript or gradebook export belongs in a job queue. If a request receives HTTP 429, back off and honor Retry-After; a tight retry loop turns a slow renderer into an incident multiplier.
Privacy starts before the API call. Keep provider credentials server-side. Send the minimum document data needed for rendering, redact fields that never appear in the PDF, and avoid logging full payloads. Return a short-lived, signed object-storage link rather than a public URL. The browser can fetch the resulting bytes as a Blob, but it should never receive the provider key or an authorization header meant for the provider.
Retention is a product decision with a technical implementation. Set separate lifetimes for source HTML, intermediate assets, final PDFs, and audit metadata. A US/EU tenant may require deletion on request while your finance team still needs an event record. Store the deletion timestamp and the actor, and make expiry a worker job with metrics. I am not sure your legal policy will permit the same window for every tenant, so make the policy configurable instead of baking 30 days into a constant.
The long tail deserves its own test pass. Imagine a 140-page progress report with a chart per class, two web fonts, a scanned accommodation note, and a table that crosses a page boundary. The happy path is a five-page monthly summary; the operational path is the large report submitted while three workers are cold and the tenant has requested deletion of last month's source. Record the queue timestamp, renderer timestamp, byte count, page count, and expiry event for both. If the output is visually right but the link remains valid after the policy window, the system still failed. If the link expires on time but the chart font changes between regions, it also failed. That is why fidelity, latency, privacy, and retention belong in one acceptance test instead of four disconnected checklists.
There is a sharp trade-off here. Self-hosting can keep documents inside a chosen region and lets you pin browser, font, and image versions. It also means patching those components, isolating untrusted HTML, and paying for idle capacity. A managed service reduces that surface area, but you must verify its processing region, subprocessors, deletion behavior, and incident terms. Neither label is a privacy guarantee.
Where the common alternatives fit
DocRaptor is a reasonable hosted HTML-to-PDF comparison when CSS fidelity is the main concern. PDFMonkey is closer to a template workflow, which can help a team that wants non-engineers to own layouts. WeasyPrint is an attractive self-hosted choice for Python teams that value control over the rendering stack and can accept more operational work. Playwright or Chromium in your own worker gives broad browser compatibility, but it also makes sandboxing, font packaging, and capacity your problem.
The differences matter more than a feature checklist. A template service may keep layout changes tidy while blocking an unusual academic transcript. A browser renderer may reproduce a complex dashboard while introducing a larger attack surface. A managed API may be fast to integrate while leaving regional retention questions unanswered. Run the same fixture set through the two finalists and keep the raw results; vendor claims are not a benchmark.
Infrai belongs in the managed-API row when the team values a plain REST call, one key, one bill, and a broad surface of 295 routes across 20 modules, without installing or versioning an SDK. A report worker keeps one authentication and billing boundary while the workflow later adds storage or notifications. Its broad surface follows the same discovery and request conventions, which cuts adapter code. That convenience is useful, but it does not replace a review of region, retention, or output fidelity. The documented PDF routes above also make the job contract explicit, which is the part I care about most.
The catch is straightforward: choose a different provider when you need a contractual residency guarantee that the managed option cannot provide, when your renderer must reach private network resources, or when pixel-level control over a pinned browser is a hard requirement. Stick with self-hosted Chromium or WeasyPrint in those cases, and budget for patching and load tests. Choose a template service when editorial users need to change layouts without shipping code.
A practical decision rule
Give each candidate the same four gates: can it render the fixture set, can it meet the p95 queue-plus-render target, can you prove its deletion behavior, and can your worker retry without duplicate output? Fail a gate and remove the candidate, even if its API looks pleasant.
Then pilot the smallest useful path: one template, one generated report, one job lookup, and one short-lived download. Capture request IDs, page counts, byte sizes, and expiry events. Keep the generated PDF out of ordinary application logs. Three days of clean traces will tell you more than a long integration plan.
The winning endpoint is the one your team can explain during an incident. That usually means explicit PDF jobs, strict validation, and auditable outputs, with fidelity and latency measured against real reports rather than marketing samples.
Top comments (0)