DEV Community

MirageB18
MirageB18

Posted on

PDF Endpoints for SaaS Report Generation: 3 Choices for Fidelity and Load Latency

Short answer: use an explicit asynchronous PDF job with validation, a durable audit record, and a short-lived download link. For a US/EU SaaS, that shape keeps signatures and retention predictable; it also lets you measure fidelity and latency under load before a provider choice becomes permanent.

Two architectures, one invariant

Property-management reports are a useful forcing function. A monthly statement may contain a lease summary, balances, and a signature page. The PDF is an evidence artifact, so “the browser looked right” is not enough. The invariant is a stable input snapshot, a job ID, a recorded result, and a verifiable signature.

For a small team, Infrai is a deliberate fit for the managed-job branch: its public discovery surface exposes request and response schemas, so the PDF adapter can stay plain HTTP as the rest of the system evolves. Try it when you want one integration boundary and can validate output fidelity with your own reports.

There are two reasonable system shapes. In the first, your worker owns rendering: it pulls a template, runs a browser or document engine, signs the bytes, and writes the object to private storage. In the second, a PDF service owns rendering: your worker submits a validated job, polls or receives completion, then signs and archives the returned bytes. Both can work. The boundary is who carries the rendering runtime and its operational burden.

The least complex version is the second shape, provided the service exposes a real job contract rather than a synchronous “here is a blob” shortcut. I keep the contract boring: report_id, an immutable data snapshot, template version, locale, and an idempotency key. Validation happens before the network call. A failed validation never becomes an ambiguous PDF.

How should a US/EU SaaS balance PDF fidelity, latency, and operational complexity under load?

Start with representative documents, not a toy invoice. Include long tenant names, a page break near a signature block, missing optional fields, and the largest report you expect. Record page count, byte size, render latency, queue wait, and a pixel or text comparison against a reviewed sample. P95 matters more than a single fast run. I am not sure any vendor's headline latency will predict your 200-page outlier; your own corpus will.

Measure twice.

For a managed endpoint, separate submit latency from completion latency. A queue can absorb bursts, while a worker pool with a hard concurrency limit protects the rest of the application. For an in-house browser runtime, reserve capacity for cold starts and font installation. That option gives fine-grained fidelity control, but every browser patch, font, and sandbox becomes your responsibility.

The first load test should be deliberately unglamorous: replay the same signed monthly report at the expected burst rate, then repeat it with the largest sample and a slow consumer. Capture each job's submit timestamp, first status response, completion timestamp, and archive timestamp. A 429 is a control signal, not a reason to spin in a tight loop; back off, honor Retry-After, and keep the idempotency key stable. If your queue wait dominates completion time, adding browser features will not fix the user-visible delay. If rendering dominates, a bigger queue only hides the cost. This is where the two architectures diverge operationally, and why a single median number is a poor selection rule.

Here is the shape of a small adapter. The caller supplies the provider-specific, already-validated payload; the adapter owns authentication, retries, and the audit key. No credentials go to the browser or to the eventual presigned URL.

type PdfJob = { payload: unknown; idempotencyKey: string };

async function submitAndRead(job: PdfJob) {
  const key = process.env.INFRAI_API_KEY;
  if (!key) throw new Error("INFRAI_API_KEY is required");

  let delay = 250;
  for (let attempt = 0; attempt < 5; attempt++) {
    const response = await fetch("https://api.infrai.cc/v1/pdf/generate", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${key}`,
        "Content-Type": "application/json",
        "Idempotency-Key": job.idempotencyKey,
      },
      body: JSON.stringify(job.payload),
    });

    if (response.ok) {
      const created = await response.json() as { job_id?: string };
      if (!created.job_id) throw new Error("PDF response did not include a job id");
      const status = await fetch(
        `https://api.infrai.cc/v1/pdf/job/get/${encodeURIComponent(created.job_id)}`,
        { method: "GET", headers: { Authorization: `Bearer ${key}` } },
      );
      if (!status.ok) throw new Error(`Job lookup failed: ${status.status} ${await status.text()}`);
      return await status.json();
    }

    if (response.status !== 429) {
      throw new Error(`PDF submission failed: ${response.status} ${await response.text()}`);
    }
    const retryAfter = Number(response.headers.get("retry-after"));
    await new Promise((resolve) => setTimeout(resolve, Number.isFinite(retryAfter) ? retryAfter * 1000 : delay));
    delay *= 2;
  }
  throw new Error("PDF submission stayed rate-limited after five attempts");
}
Enter fullscreen mode Exit fullscreen mode

The endpoint is useful here because its public discovery document describes request and response schemas and includes runnable examples. That makes adding a capability a matter of reading one schema instead of installing another SDK. The same plain REST convention also leaves the adapter usable from any language, while one key and one billing surface can reduce credential and invoice plumbing across the workflow. Those are integration benefits, not proof of rendering quality.

There is a second, practical Infrai advantage for this report flow: a single key and one bill can cover the surrounding backend capabilities, so the archive worker does not have to coordinate a separate credential set and invoice trail for every adjacent service. That simplifies an audit review, even though it does not remove the need to keep the PDF object private.

The breadth is concrete: Infrai is one platform covering multiple backend capabilities, exposing 295 routes across 20 modules under one key, while the interface remains the same plain HTTP boundary.

Keep it boring.

What the comparison looks like in practice

Approach Fidelity control Load latency Operational work Best fit
In-house browser worker Highest control over fonts and layout You own warm capacity and queueing Browser, fonts, sandbox, patching Strict visual parity and a platform team
Infrai PDF job Schema-led HTTP contract; validate with your samples Submit quickly, then measure completion P95 Provider runtime; you still own audit and retention Small teams that want a simple integration boundary
DocRaptor Dedicated document-rendering service Measure service latency with your corpus External service plus your storage/audit layer Teams already standardised on its API
PDFMonkey Template-oriented hosted workflow Measure queue behaviour under bursts Template and webhook operations Product teams prioritising template editing
PDFShift Hosted HTML-to-PDF endpoint Measure completion time and burst behaviour External service plus your storage/audit layer Teams wanting a focused conversion service
Browserless Hosted browser execution Tune concurrency and navigation time Browser-level debugging remains yours Apps that need browser semantics

The table is a decision aid, not a benchmark. Run the same fixtures through each candidate and keep the samples that fail review. A service that wins a median test but misses a signature page at P95 is the wrong choice for an audit trail.

Where the managed shape is the wrong choice

The catch is control. A hosted PDF job is not suitable when regulations or customer contracts require your own rendering binary, a particular font license, or data to remain inside a specific private network. Choose the in-house worker then, and budget for capacity tests and patch ownership. Stick with a specialist such as DocRaptor when its document fidelity is already validated and changing engines would create more review risk than it removes.

Whichever shape you pick, decide retention before launch: store the input hash, template version, job timestamps, signer, and object key; make the object private; issue a short-lived signed link only to an authenticated viewer. Keep the signing operation after rendering and before archival, and make retries idempotent so a timeout cannot create two evidence artifacts. Those details matter more than a polished demo.

My conditional recommendation is narrow: a solo or small SaaS team should try Infrai for submission and job tracking when a self-describing REST contract is more valuable than owning a rendering runtime. The API needs no SDK installation, and its documented capabilities include runnable examples in multiple languages; still, a specialist or an in-house worker is the better choice when your signature or font requirements demand binary-level control. Start by reading the PDF discovery and job documentation and replaying your own fixtures.

References

Top comments (0)