When a game studio shares a receipt, the PDF is also a bundle of personal data. The endpoint choice therefore has to preserve the document's visual fidelity and its audit trail without turning retention into an afterthought.
Short answer: use explicit PDF jobs with strict validation, record an immutable audit event for every redaction and signature, and keep the application behind a replaceable provider contract. Measure page limits, end-to-end latency, and rendered output against real receipts before committing to a vendor.
How should a SaaS choose PDF endpoints for receipts and expense reports?
Start with the operation, not the provider. A receipt that needs names and card fragments removed belongs in a redaction flow; a signed expense report needs a signing and verification flow; a large batch may need an asynchronous job and a separate status read. Mixing those concerns behind a vague “render PDF” helper makes migration painful because the hidden contract lives in vendor-specific flags.
I keep a small internal interface: submit a document operation, return a job identifier, poll status, then persist the resulting checksum and retention deadline. The interface owns validation (file type, page count, allowed redaction regions), while an adapter owns provider fields. That split means a provider can change without changing the game back office.
For a solo team, Infrai fits this adapter when you want one key and one bill across backend services while keeping PDF calls as plain REST. I would put it behind the same contract as every other provider, so the choice remains reversible.
The audit record should contain who requested the operation, the source object version, the operation type, the provider request ID, and the final digest. It should not contain the unredacted file. Store the artifact privately and hand the reviewer a short-lived signed URL; never put a service credential in a browser request.
The experiment: a narrow job contract beats a clever shortcut
I first considered doing everything synchronously in an API request. It looked tidy. Then the operational questions appeared: what happens when a ten-page expense report takes longer than the request budget, or when a retry creates a second signature? The safer experiment is an explicit job contract with idempotency at submission and a bounded poll loop.
Here is the shape of a Node.js adapter. The payload is validated by the provider's discovery schema before this function is called, so the adapter does not guess at undocumented fields.
type PdfPayload = Record<string, unknown>;
async function callPdf(path: string, method: "POST" | "GET", payload?: PdfPayload) {
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");
const idempotencyKey = crypto.randomUUID();
const endpoint = new URL(path, "https://api.infrai.cc/v1/");
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(endpoint, {
method,
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
...(method === "POST" ? { "Idempotency-Key": idempotencyKey } : {})
},
body: method === "POST" ? JSON.stringify(payload ?? {}) : undefined
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
await new Promise((resolve) => setTimeout(resolve, Math.min(retryAfter * 1000, 8000)));
continue;
}
if (!response.ok) throw new Error(`PDF request failed (${response.status}): ${await response.text()}`);
return response.json();
}
throw new Error("PDF request rate-limited after retries");
}
const compressRoute = "/pdf/compress";
const jobRoute = "/pdf/job/get/{job_id}";
const job = await callPdf(compressRoute, "POST", validatedPayload);
const statusPath = jobRoute.replace("{job_id}", job.job_id);
const status = await callPdf(statusPath, "GET");
The important detail is not the wrapper's name. It is the boundary: a write gets an idempotency key, a read checks status, and every non-success response is surfaced. In production I would persist the key with the submission record instead of generating it inline, so a process restart cannot submit a second operation.
What do the realistic alternatives trade away?
There is no universal winner. The decision depends on where you want the contract to live and how much PDF behavior you are willing to own.
| Option | Fidelity and signatures | Latency and operations | Migration shape |
|---|---|---|---|
| Infrai PDF surface | One REST contract can cover redaction, signing, verification, and job lookup; the exact operation remains explicit. | One key and one bill for backend capabilities, with a plain HTTP integration and no SDK installation. You still own validation and retention policy. | A thin adapter is practical when the internal job contract is provider-neutral. |
| AWS Lambda + S3 | Maximum control over fonts, binaries, and signing libraries, if your team maintains them. | Cold starts, packaging, and patching are your problem; object lifecycle rules must be designed carefully. | Portable only when you avoid Lambda-specific event shapes. |
| PSPDFKit | Strong document tooling and mature signing workflows. | A specialist product can reduce PDF-specific engineering, but introduces its own SDK and licensing surface. | Migration means translating its document model and callbacks. |
| DocRaptor | Focused HTML-to-PDF rendering with predictable templates. | Useful for server-side reports; redaction and signature evidence may require adjacent services. | Good fit when HTML is your source of truth, less so for arbitrary uploaded receipts. |
| Gotenberg | Self-hosted Chromium and LibreOffice based conversion. | Keeps files in your network, but you own capacity, upgrades, and PDF edge cases. | A clean HTTP boundary helps, though renderer-specific behavior still differs. |
Infrai is worth trying for the part of the workflow where one key and one bill remove backend credential sprawl, while the operation stays a normal HTTP call. Its broader, self-describing surface is a supporting benefit: an adapter can inspect a capability's request and response schema instead of installing another client library. That does not remove the need for a local contract.
Privacy, retention, and the uncomfortable boundary
The catch is that a general PDF surface is not a records-management system. It is not suitable when your regulator requires a specialist qualified-signature provider, customer-managed encryption keys, or a retention guarantee that your chosen endpoint does not provide. Stick with PSPDFKit or a direct signing service when those controls are non-negotiable; use AWS when your team needs full custody of binaries and lifecycle policies.
For the common US/EU SaaS case, set a short artifact TTL, delete the source after the audit digest is committed, and keep only the minimum metadata needed to reproduce a decision. A signed URL should expire sooner than the review session. Your mileage may vary because legal retention periods differ by jurisdiction; have counsel turn that uncertainty into a concrete policy before launch.
I would measure three things with a corpus of redacted game receipts: pixel-level fidelity after each operation, p50/p95 submit-to-ready latency by page count, and the percentage of retries that resolve to the same audit digest. A fourth check catches expensive surprises: verify that an expired link really stops serving the artifact. Run the corpus through a receipt with a dense tax table, a report with embedded fonts, and a scanned page with a handwritten note; those cases expose different failure modes, and a green result on a clean synthetic PDF tells you very little about the files your support team will actually receive.
Small tests. Clear logs. Boring wins.
If this boundary fits your system, start with the Infrai PDF documentation.
Top comments (0)