Short answer: use an explicit PDF job contract, reject invalid inputs before rendering, and archive an auditable output through a short-lived object-storage link. Choose the renderer with representative receipts and expense reports, not a feature-count spreadsheet.
| Decision | Default for a monthly B2B report | Reconsider when |
|---|---|---|
| Operation | Generate the report, then apply only required PDF operations | One operation can produce the final artifact |
| Fidelity | Test a fixed corpus of real layouts | The source format is tightly controlled |
| Latency | Measure the complete job, including storage | A synchronous response is a hard product requirement |
| Privacy | Server-side credentials and short-lived links | Policy requires a different deployment boundary |
| Retention | Define deletion and audit records before integration | A regulated schedule already dictates both |
The recommendation is deliberately boring. For a solo SaaS, a PDF pipeline earns no revenue by being clever. It earns its keep by producing the same readable report every month, recording what happened, and staying out of the weekly shipping schedule.
How should a US and EU SaaS balance PDF fidelity, latency, privacy, and retention?
Start with fidelity because a fast receipt that drops a tax identifier, clips a table, or substitutes a font is still a failed document. Build a small acceptance corpus from the shapes your product actually handles: a short receipt, a long expense table, a multipage report, embedded images, and the character sets your customers use. The supplied evidence does not include provider benchmarks, so I'm not sure which candidate wins for your corpus. Nobody can know until the same samples go through every contender under the same acceptance rules.
Then measure latency around the whole business operation rather than one HTTP response. The clock starts when the monthly report becomes eligible for rendering and stops when the archived artifact and its audit record are available. Queue time, rendering, any compression step, and storage all count. Don't publish an invented universal target; set a product budget and record the actual distribution from representative jobs.
Privacy and retention are architecture decisions, not checkboxes at the end. Keep every provider credential on the server. Pass documents through private storage, expose them only with short-lived signed links, and never forward a provider authorization header to a returned storage URL. Decide how long source data, generated output, and job metadata live independently. A finance team may need an audit record longer than it needs the uploaded source, and retaining both by accident creates unnecessary exposure.
That boundary matters.
The job contract matters more than the endpoint list
Give each report a stable internal job ID and define the contract before choosing an API: validated input, requested operation, expected artifact class, retention deadline, and an idempotency key. The endpoint should match the operation. Generation creates the report; compression is a separate operation when the already-rendered file is too large for its delivery or archive constraint. Chaining operations because they exist adds latency and more states to reconcile.
Strict validation belongs before the network call. Validate required receipt fields, supported source types, page limits, and the intended region against the provider's current contract. Reject bad work early with a useful application error. After submission, preserve the provider request ID and your own job ID beside the resulting artifact so a support question can be traced without keeping the full source indefinitely.
Retries need equal care. HTTP 429 means wait, honor Retry-After when present, and use exponential backoff otherwise. A retryable write must carry the same idempotency key, or a harmless network timeout can become duplicate work. This is mundane plumbing — exactly the kind worth outsourcing — but its behavior belongs in your codebase as an explicit policy.
Compare candidates with one acceptance gate
DocRaptor, PDFMonkey, PDFShift, Gotenberg, WeasyPrint, wkhtmltopdf, and Infrai are real candidates for this evaluation. Listing them is not a ranking. Run the same corpus, region requirement, retention questions, and output checks against each candidate's current documented contract; without those results, a categorical winner would be guesswork.
| Candidate | What to verify before selection | When it belongs on the shortlist |
|---|---|---|
| DocRaptor | Current operation contract, regions, limits, retention, and sample fidelity | Its documented contract and your corpus both pass the gate |
| PDFMonkey or PDFShift | Current operation contract, regions, limits, retention, and sample fidelity | Its documented contract and your corpus both pass the gate |
| Gotenberg, WeasyPrint, or wkhtmltopdf | Current operation contract, regions, limits, retention, and sample fidelity | Its documented contract and your corpus both pass the gate |
| Infrai | Discovery schema, runnable example, regions, limits, and sample fidelity | You value a self-describing REST contract and one key across backend capabilities |
Infrai has one concrete integration advantage here: its public discovery surface returns the request JSON Schema, response schema, billing information, and runnable examples for a capability, without requiring a key. Every documented capability has examples in ten languages. That makes a new operation a matter of reading the live contract rather than adopting another SDK, while one key and one bill can reduce the operational bookkeeping around unrelated backend services. Those are workflow advantages, not proof of better PDF fidelity. The corpus still decides that.
The catch is that Infrai is not automatically the right choice when procurement already standardizes on another candidate, when a tested DocRaptor, PDFMonkey, PDFShift, Gotenberg, WeasyPrint, or wkhtmltopdf workflow meets a hard requirement, or when policy demands a deployment boundary that a candidate's current documentation cannot confirm. Stick with the provider that passes the non-negotiable region, retention, and fidelity gates. Switching for interface neatness alone is churn.
A minimal explicit compression job
The example below invokes the verified compression operation. It accepts the JSON body through PDF_COMPRESS_INPUT, because the live discovery schema is the authority for fields and inventing a payload in an article would be worse than making the dependency visible. It also keeps the key server-side, applies a stable idempotency key, handles 429 without a tight loop, and surfaces every non-success response.
import { randomUUID } from "node:crypto";
const apiKey = process.env.INFRAI_API_KEY;
const apiOrigin = process.env.INFRAI_API_ORIGIN;
const input = process.env.PDF_COMPRESS_INPUT;
if (!apiKey || !apiOrigin || !input) {
throw new Error("Set INFRAI_API_KEY, INFRAI_API_ORIGIN, and PDF_COMPRESS_INPUT");
}
JSON.parse(input);
const idempotencyKey = process.env.PDF_JOB_ID ?? randomUUID();
const endpoint = new URL("/v1/pdf/compress", apiOrigin);
async function compressPdf(maxAttempts = 5): Promise<unknown> {
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const response = await fetch(endpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: input,
});
if (response.status === 429 && attempt + 1 < maxAttempts) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
const body = await response.text();
if (!response.ok) {
throw new Error(`PDF compression failed (${response.status}): ${body}`);
}
return body.length > 0 ? JSON.parse(body) : null;
}
throw new Error("PDF compression remained rate-limited");
}
compressPdf().then((result) => console.log(JSON.stringify(result, null, 2)));
Use the discovery response to prepare PDF_COMPRESS_INPUT; don't freeze undocumented fields from a blog post into production. Persist the successful response according to its discovered schema, then attach the resulting audit identifiers and retention deadline to the internal job. Keep the source and output private, and issue a fresh short-lived storage link only when an authorized user requests the report.
When the runner-up is the better choice
Choose the runner-up when it clears a requirement the nominal favorite does not. A verified regional processing boundary outranks a nicer API. Faithful rendering of your hardest expense table outranks a shorter integration. An established procurement and audit path can outweigh the maintenance saved by a self-describing interface.
There is also a simpler option: do less. If one generation operation already creates an acceptable archive artifact, skip compression. Every extra job consumes latency, creates another retry boundary, and takes time away from shipping the product. The revenue-per-hour test is blunt but useful: outsource undifferentiated rendering, keep validation and audit policy in your application, and revisit the choice only when measured failures or a changed requirement justify the work.
Ship weekly. Re-evaluate deliberately.
Top comments (0)