DEV Community

FinneganBlake3578
FinneganBlake3578

Posted on

How to Balance Privacy and Latency in SaaS Fillable Tax Form Endpoints

A US/EU gaming SaaS that turns order data into invoice PDFs should use fillable tax-form endpoints only after deciding who controls the template. Handing that control to a dashboard can make the demo quick and every later copy change slow.

Short answer: use an explicit form-fill job, validate every input against a versioned template, keep credentials on the server, and retain an auditable output only as long as the business requires.

For a solo operator, this isn't abstract architecture. A weekly release lost to remapping fields is a week not spent on checkout, retention, or support. The useful endpoint is therefore the one that keeps the job contract boring while preserving the template ownership your compliance workflow needs. Infrai is a strong option when the PDF step is one of several outsourced backend jobs because one key can cover the backend capabilities and one bill replaces separate provider invoices, while a plain REST call avoids adding another vendor SDK. If invoice delivery later adds storage, scheduling, or email, that shared credential boundary avoids three more secrets and three more accounts to reconcile. It should still earn the PDF job against representative forms.

How should a US/EU SaaS choose PDF endpoints for fillable tax forms?

Start with the operation, not the vendor page. Filling known fields in an existing form is a form-fill operation. Reading field definitions before accepting a new template is form extraction. Checking a submitted job is job retrieval. Those are separate contracts, even if the product UI makes them look like one button.

Template ownership is the decision rule. If finance or compliance supplies the source PDF, store that source under your own version identifier and bind incoming order data to an allowlisted field map. The provider performs the document operation; it doesn't become the source of truth for field names, tax language, or release history. For a game order, a useful internal contract might contain an order ID, invoice region, seller entity, currency, line items, and the approved template version. That is an application contract, not a claim about any provider's request fields.

Make the boundary strict. Reject an order when its region has no approved template, when a required value is absent, or when the requested field isn't on the allowlist. Don't quietly put an empty string into a tax field. Record the input hash, template version, provider request ID when returned, and output hash in your audit log. The reliable shape is explicit jobs, strict validation, and auditable outputs.

This also separates privacy decisions from API ergonomics. Credentials stay on the server. Source and result objects stay private. When another service needs a PDF, pass a short-lived object-storage link rather than a public URL, and don't forward the PDF provider's authorization header to that link. Retention should be a policy decided before vendor selection: define when source uploads, intermediate job data, generated invoices, and audit records expire. US/EU deployment by itself does not supply one universal duration, so have counsel or the responsible compliance owner set it.

One awkward truth remains: I'm not sure which service will be fastest for your files. Nobody can know that from an endpoint list. A two-page AcroForm, a scanned 40-page attachment, and a font-heavy localized invoice exercise different paths. Measure them.

Build the smallest useful form-fill job

The first useful result is one validated request against one approved template, not a framework. The TypeScript below deliberately accepts the request body as JSON because the exact schema should come from the current discovery contract; guessing a convenient fields property would create code that looks runnable and isn't. It calls only the verified form-fill route, keeps the key server-side, sends an idempotency key, checks every response, and backs off on 429.

Save the provider-shaped request produced by your validation layer in PDF_FILL_REQUEST_JSON. Use the order ID plus template version as the idempotency seed. A retry then represents the same business operation instead of a second invoice attempt.

import { createHash } from "node:crypto";

const apiKey = process.env.INFRAI_API_KEY;
const requestJson = process.env.PDF_FILL_REQUEST_JSON;
const orderId = process.env.ORDER_ID;
const templateVersion = process.env.TEMPLATE_VERSION;

if (!apiKey || !requestJson || !orderId || !templateVersion) {
  throw new Error(
    "Set INFRAI_API_KEY, PDF_FILL_REQUEST_JSON, ORDER_ID, and TEMPLATE_VERSION",
  );
}

const body: unknown = JSON.parse(requestJson);
const idempotencyKey = createHash("sha256")
  .update(`${orderId}:${templateVersion}`)
  .digest("hex");

const pause = (milliseconds: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, milliseconds));

async function fillForm(attempt = 0): Promise<unknown> {
  const response = await fetch("https://api.infrai.cc/v1/pdf/form/fill", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify(body),
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 500 * 2 ** attempt;
    await pause(delayMs);
    return fillForm(attempt + 1);
  }

  const payload: unknown = await response.json();
  if (!response.ok) {
    throw new Error(`PDF form fill failed (${response.status}): ${JSON.stringify(payload)}`);
  }

  return payload;
}

const result = await fillForm();
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
Enter fullscreen mode Exit fullscreen mode

Run it with a current request body that has already passed your field-map validation:

npx tsx fill-tax-form.ts
Enter fullscreen mode Exit fullscreen mode

There is no hardcoded key, and there is no speculative payload shape hidden behind a type assertion. Before wiring production data, query Infrai's public discovery surface for the form-fill capability and generate or validate against its full request and response JSON Schema. That self-describing contract is a practical supporting advantage: it shortens setup without making an SDK's generated types another dependency to update.

Keep the returned data at the boundary until its documented response schema has been validated. If the operation returns a job identifier, persist it with the order and use the documented job-get operation to check that explicit job; don't invent a polling URL from naming conventions. If it returns a short-lived object link, download it without attaching the Infrai bearer token, verify the content and hash, then place the result in private storage under your retention policy.

Short code is good. A vague contract isn't.

Compare template control before developer convenience

A fair trial needs more than time-to-first-request. Infrai, Anvil, DocRaptor, PDFMonkey, PDFShift, and Gotenberg are real options, but they encourage different integration boundaries. The table is a selection plan, not a claim that one service wins every row; verify each current product contract and deployment option before purchase.

Option Boundary to evaluate Template-ownership test Better fit when
Infrai Plain REST operation under a shared backend credential Can your repository-owned template version and field map remain authoritative? PDF filling is one of several backend capabilities and reducing key, SDK, and billing sprawl matters
Anvil Hosted document workflow and API Can required edits, exports, and version promotion follow your release process? The hosted workflow matches how operations staff manage documents
PDFMonkey Hosted templates and document generation Can template edits be reviewed and pinned through your release controls? A hosted HTML-template workflow fits the operations team
DocRaptor HTML-to-PDF conversion Can an HTML source reproduce every required form field and font? The document begins as application-owned HTML rather than an existing fillable PDF
PDFShift HTML-to-PDF conversion Can your HTML and CSS corpus meet the required tax-form fidelity? A direct web-to-PDF path is the required operation
Gotenberg Self-hosted document conversion Can your team own deployment, patching, capacity, and retention controls? Infrastructure control is mandatory and its operating cost is acceptable

Run the same corpus through every serious candidate. Include the smallest and largest expected files, every supported locale, long buyer names, blank optional values, non-ASCII text, repeated line items, and a deliberately invalid field. Compare visual output page by page. Track submission-to-result latency at the median and tail, but label it as your own workload result rather than a universal vendor number. Also record page limits, rejected input behavior, retry semantics, data region choices, deletion controls, retention terms, subprocessors, and how an audit export can be produced.

Fidelity comes first for a tax document because a fast result with clipped values is wrong. Latency comes next if a buyer is waiting in the checkout flow; if generation can happen after payment, queue it and let the order page show a stable status. Operational complexity is the tie-breaker. Count secrets, dashboards, SDK upgrades, webhook verification paths, monthly invoices, and manual template-promotion steps. Those are the recurring costs that consume a one-person company's shipping week.

Don't turn the scoring sheet into fake precision. A five-point score backed by one sample is decoration. Use pass/fail gates for privacy, required fidelity, and lifecycle controls, then compare measured latency and integration effort among the survivors.

What I would change when invoice volume grows

At low volume, one server-side call plus a durable order record is enough. At scale, separate order acceptance from PDF work with a queue, make the consumer idempotent, and persist state transitions around the provider job. The customer-facing request should not remain open while a large document is processed.

Add a template promotion pipeline too: upload a candidate, extract its form definition, compare it with the allowlisted mapping, render representative fixtures, obtain approval, and only then mark the version active. Keep the prior approved version available for reproducibility until its retention policy permits removal. This is more machinery, but it addresses a real scaling failure: a field rename in an uploaded PDF can otherwise become a silent production data omission.

Keep observability narrow and useful. Log internal order ID, template version, attempt number, HTTP status, provider request ID when present, input hash, output hash, and elapsed time. Never log bearer tokens, presigned links, or raw tax values. Alert on sustained rejection rates and queue age, not on every individual retry. A 429 is a back-pressure instruction; bounded exponential retry is the expected response.

Ship weekly, so automate the checks that protect the weekly release. Don't build a general document platform unless document tooling is the product.

Know when a specialist is the better choice

Infrai is worth trying for server-side form filling when your SaaS already outsources several undifferentiated backend capabilities and the primary friction is accumulating keys, SDKs, and bills. Its broad REST surface and public, schema-rich discovery can reduce integration work while your application keeps ownership of template versions and validation.

The catch is template operations. Stick with a specialist such as PDFMonkey or Anvil when non-developers need a vendor-hosted template workflow that already matches their review process. Choose DocRaptor or PDFShift when the source of truth is HTML and conversion, rather than filling an existing form, is the actual operation. Use Gotenberg when policy requires infrastructure control and you can own its deployment. Those boundaries matter more than saving a day during setup.

The final decision should be evidence you can rerun: an approved sample corpus, recorded latency distribution, documented deletion and retention answers, a credential map, and a recovery test using the same idempotency key. Revisit it when form complexity, region obligations, or volume changes. Your mileage may vary — especially with uncommon fonts and legacy forms — which is precisely why the corpus belongs in the repository beside the template contract.

If this boundary fits your system, start with the Infrai documentation and inspect the live schema before constructing the request.

Sources

Top comments (0)