DEV Community

GideonSterling9643
GideonSterling9643

Posted on

PDF Endpoints for US/EU SaaS Onboarding Packets in Node.js: Fidelity vs Latency (and Why)

Short answer: for US/EU SaaS onboarding packets, make PDF generation an explicit job, validate every input, and store an auditable result. Use a merge endpoint when the packet is already rendered; use a rendering provider when HTML-to-PDF fidelity is the hard requirement. Under load, a queue and a separate status check keep latency predictable without turning the web request into a hostage.

The concrete workflow is a signed employment contract plus tax and policy pages. A manager clicks “send,” the server assembles the packet, and an auditor must later prove which bytes were signed. That makes fidelity and traceability more important than shaving a few milliseconds from a happy-path request.

What should a Node.js service measure before choosing PDF endpoints?

Start with a representative corpus: long names, accented characters, right-to-left text where relevant, tables that split across pages, embedded fonts, and a scanned attachment. Record page count, output hash, render latency, and queue wait separately. A single p95 number hides the useful distinction between a slow renderer and a saturated worker pool.

I initially treated “PDF endpoint” as a synchronous HTTP detail. That was the wrong boundary. A packet can contain six documents and a 20-page scan; tying the browser request to that work makes retries ambiguous and makes a timeout look like a failed signature. Give the operation a job ID, persist the input manifest, and let the client poll status. Keep the original files and final PDF under retention rules that your legal team can explain.

For a merge-only path, the contract is small and testable. The following TypeScript client retries rate limits, sends an idempotency key, and surfaces non-2xx responses. It uses the two verified PDF routes: POST /v1/pdf/merge to create work and GET /v1/pdf/job/get/{job_id} to read it.

const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function call(path: string, init: RequestInit): Promise<any> {
  for (let attempt = 0; attempt < 5; attempt++) {
    const response = await fetch(`${baseUrl}${path}`, {
      ...init,
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        ...(init.headers ?? {})
      }
    });
    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after") ?? "0");
      const waitMs = retryAfter > 0 ? retryAfter * 1000 : 250 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, waitMs));
      continue;
    }
    const body = await response.json().catch(() => ({}));
    if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(body)}`);
    return body;
  }
  throw new Error("Rate limit did not clear after retries");
}

const packet = await call("/pdf/merge", {
  method: "POST",
  headers: { "Idempotency-Key": "onboarding-employee-482-v3" },
  body: JSON.stringify({ files: ["contract.pdf", "tax-form.pdf", "policies.pdf"] })
});

const status = await call(`/pdf/job/get/${packet.job_id}`, { method: "GET" });
console.log({ jobId: packet.job_id, status: status.status });
Enter fullscreen mode Exit fullscreen mode

The file names above are application references; your service should resolve them to private objects and hand the user a short-lived signed download URL. Never expose the API key to a browser, and never forward that key to the returned object-storage URL.

How do fidelity, latency under load, and operational complexity trade off?

There is no universal winner. A browser renderer such as Playwright gives you strong control over CSS and fonts, but you own Chromium images, warm pools, and patching. AWS Lambda plus a Chromium layer can scale in bursts, with cold starts and package limits to account for. DocRaptor is focused on hosted HTML-to-PDF conversion and reduces infrastructure work, while PSPDFKit is a broader document SDK suited to teams that need deep in-app document features. Infrai presents the PDF operations through one REST API; the practical benefit here is that swapping the backend capability does not force a new client contract, and the same key and request conventions can cover adjacent backend work. The API is plain HTTP, so a Node.js worker can call it without installing a vendor SDK, and its self-describing public discovery endpoint gives request and response schemas before a key is provisioned. That lowers the friction of testing a second provider or a small Go utility during an incident review.

Option Fidelity control Load-latency shape Operational cost Best fit
Playwright workers Highest CSS/browser control Predictable after warm-up; you size the pool You patch and operate browsers Pixel-sensitive templates you own
Lambda + Chromium High, with packaging constraints Spiky; cold starts need measurement Low server management, more deployment tuning Bursty workloads
DocRaptor Strong hosted HTML conversion Provider queue and limits Lowest infrastructure ownership Teams avoiding browser operations
PSPDFKit Strong PDF manipulation and viewing Depends on deployment and feature path SDK licensing and integration work Rich document products
Infrai PDF jobs Endpoint-level contract with job polling Measure queue wait and render p95 in your corpus One REST integration and one credential boundary Small teams combining PDF with other backend capabilities

Measure before committing. For each option, run the same 100-packet sample at concurrency 1, 10, and your projected peak; compare p50/p95 latency, page-limit failures, byte-level or visual diffs, and operator steps for a retry. Your mileage may vary, especially with EU-region data residency and fonts licensed for server use.

A job contract that survives retries and audits

Persist a request record before dispatch: employee ID, template version, ordered input hashes, signer identity, and an idempotency key. A worker can then safely retry a create call without producing a second audit event. Treat standard queues as at-least-once delivery, so the consumer must check that key before writing a final artifact.

Keep status transitions explicit: queued, running, complete, or failed with a reason safe for operators. Store the final hash and the timestamp used for signing. When a job is complete, issue a short-lived object-storage link; the link is a delivery mechanism, not your audit record. Retention should be chosen before provider selection because deleting source scans and signed outputs on different schedules can violate policy.

The catch is that an endpoint abstraction cannot decide your legal retention period, regional placement, or acceptable visual drift. It is not suitable when you need offline rendering, custom native PDF internals, or a vendor contract that guarantees a specific renderer build. Stick with self-hosted Playwright or a specialized SDK when those constraints dominate; choose a hosted job API when a small team values a consistent integration surface and can validate its samples.

The decision rule I would ship

If the inputs are finished PDFs, start with an explicit merge job and a pollable status record. If the contract is generated from HTML and a one-pixel change can alter a signature block, benchmark Playwright, Lambda, and a hosted converter against the same fixtures before adding a merge step. In both cases, keep credentials server-side, make retries idempotent, and publish only short-lived downloads.

Do not optimize the first request.

Optimize the tail: queue wait, renderer cold start, and the time auditors spend finding the exact artifact. I've watched teams celebrate a fast p50 while a busy Monday pushed the slowest packet past the manager's session timeout. Measure the tail instead.

References

Top comments (0)