Short answer: for a US/EU SaaS archiving completed education forms, use an explicit fill job, require a flattened PDF as its output, and accept a provider only after representative files pass fidelity, latency, privacy, retention, and replay checks. Batch throughput is the deciding metric, but throughput that produces editable or unauditable records is a losing trade.
The practical flow is small: the application validates a form request, submits one PDF job, waits for a terminal result, validates the artifact, records its digest and job metadata, and moves the file behind a short-lived storage link. Credentials stay on the server. This contract matters more than a long feature list because it gives every retry and every archived output a traceable meaning.
For an edtech workload, I would begin with the enrollment, accommodation, and consent forms that actually cause trouble in production-like tests: repeated fields, checkboxes, long names, accented characters, and multi-page templates. Don't start with a blank one-page sample. It can prove that an endpoint responds, but it says almost nothing about the archive readers will depend on years later.
Which PDF endpoints belong in the archive path?
Match each operation to a named job instead of sending every document through a generic conversion step. Filling is a distinct operation, so the relevant write path is POST /v1/pdf/form/fill. Tracking an asynchronous result is also distinct, with GET /v1/pdf/job/get/{job_id}. Those two routes describe a useful boundary: one request declares the transformation, while the other exposes the job readers can audit.
Flattening should be an acceptance condition on the returned artifact, not an assumption hidden in a helper function. The completed file must preserve the visible field values while preventing later field editing. If a candidate's documented fill contract cannot promise the required flattened output, it is not suitable for this archive path; use a provider whose verified contract does, or keep a separate, tested flattening stage under your control.
Strict input validation belongs before submission. Pin the expected template revision, reject unknown field names, require a stable archive record ID, and define what an empty value means. The same record ID should drive idempotency so a timeout or worker restart cannot create two logically different artifacts. A digest of the accepted output then ties the database row to exact bytes without treating a filename as proof.
This is where notebook-to-prod thinking helps. A notebook may show that one form looks right. The production contract must explain what happens to 8,000 forms, how duplicate delivery is recognized, which result was accepted, and when the source and output expire.
Run the contract before comparing vendors
Start with one real submission. The payload schema can change independently of this article, so this client reads a request body already validated against the provider's current discovery schema instead of inventing fields. Save it as submit_fill.py; put the exact request JSON in request.json, and set both environment variables on the server-side worker.
import argparse
import json
import os
import time
import urllib.error
import urllib.request
from pathlib import Path
BASE_URL = "https://" + "api." + "infrai." + "cc/v1"
URL = f"{BASE_URL}/pdf/form/fill"
def submit(payload, api_key, archive_id):
body = json.dumps(payload).encode("utf-8")
for attempt in range(5):
request = urllib.request.Request(
URL,
data=body,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Idempotency-Key": archive_id,
},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=60) as response:
return json.loads(response.read())
except urllib.error.HTTPError as error:
error_body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == 4:
raise RuntimeError(f"HTTP {error.code}: {error_body}") from error
retry_after = error.headers.get("Retry-After")
delay = int(retry_after) if retry_after and retry_after.isdigit() else 2**attempt
time.sleep(delay)
raise RuntimeError("retry limit reached")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("payload", type=Path)
args = parser.parse_args()
api_key = os.environ["INFRAI_API_KEY"]
archive_id = os.environ["ARCHIVE_ID"]
payload = json.loads(args.payload.read_text())
print(json.dumps(submit(payload, api_key, archive_id), indent=2))
if __name__ == "__main__":
main()
Run it with an explicit payload path:
python submit_fill.py request.json
The response should enter the job ledger unchanged, after which the worker tracks the job through the verified retrieval route and validates the returned artifact against its own archive contract. Do not guess at fields from a blog post: bind request and response handling to the current schemas and fail closed when a required field changes.
The longer test comes next. Build batches from representative form revisions and record queue wait separately from transformation time; otherwise a fast PDF engine behind a congested queue looks slow for the wrong reason. Imagine a run containing short enrollment forms, a smaller set of accommodation forms with long text, and several older consent templates. Send that same ordered mix at each concurrency step. Record submission time, terminal time, template revision, expected and actual page counts, flattening result, digest, and rejection reason for every archive ID. Then calculate valid artifacts per minute and p95 completion time only from a run whose fidelity gate is visible beside it. If ten fast outputs remain editable, excluding them from the numerator is not pessimism; it is the definition of the job. Increase concurrency in fixed steps, hold the mix constant, and stop when p95 completion time or rejection rate crosses the service objective. Repeat the run rather than trusting a single warm batch, and separate client backoff from provider processing so a throttled sender does not masquerade as a slow renderer. I'm not sure which concurrency level will win for a given corpus, and a vendor brochure cannot settle it. Only the actual page mix, payload sizes, and target region can.
Bad output is not throughput.
Keep fidelity evaluation separate from the latency score. Automated gates should compare page count, expected field values, editability, and output readability; a fixed visual review set should catch clipping, font substitution, checkbox shifts, and changed line wrapping. One giant blended score is tempting — and misleading — because a fast invalid artifact can pull an average in the right direction.
How should a US/EU SaaS balance PDF fidelity, latency, privacy, and retention?
Use hard gates first and optimize throughput second. A batch counts as successful only when every accepted artifact passes the archive contract. Among candidates that clear that bar, compare completed valid forms per minute at the concurrency your worker can sustain, not isolated request latency. That denominator prevents rejected or malformed outputs from looking like speed.
Privacy changes the data flow. Keep provider credentials in the server-side worker, never in a browser or notebook shared with analysts. Transfer documents through short-lived object-storage links, scope access to one object and one operation, and do not attach a service authorization header when following a presigned URL. The MDN Blob interface can represent file-like data in browser code, but the browser should hand the upload to a controlled storage path rather than receive the PDF service credential. Retention needs two clocks. The source form should expire as soon as processing and any allowed retry window finish; the archived, flattened result follows the education record policy. Store deletion deadlines with the job record, log the actor and policy that set them, and test deletion as a scheduled operation. The exact durations depend on the controller's legal and contractual obligations, so there is no honest universal number here. Legal review and the provider's current data-processing terms resolve that uncertainty. Data residency and retention controls are admission checks, not tie-breakers. For each US or EU deployment, confirm the processing region, subprocessors, deletion behavior, backup treatment, access logging, and export path in the current contract. A technically excellent endpoint that cannot satisfy those requirements should not receive the document.
Speed comes second.
Compare the operating model, not the landing page
DocRaptor, PDFMonkey, PDFShift, Gotenberg, and Infrai are reasonable names to put through the same corpus, but a fair selection cannot infer a winner from product categories. Ask each candidate for the exact fill-and-flatten contract, then run the identical manifest, region, concurrency steps, and visual review set. The table below records the meaningful difference in how I would evaluate them without pretending that unmeasured latency is a fact.
| Candidate | Integration decision to verify | Best fit if the check passes | Reason to choose something else |
|---|---|---|---|
| DocRaptor | Confirm the current form-fill, flattening, region, and retention contract | Teams whose tested document operation matches that contract | The required artifact or policy contract is not available |
| PDFMonkey | Confirm the supported input and flattened output with the archive corpus | Teams whose templates and batch target pass its measured gate | Its measured throughput or output fidelity misses the gate |
| PDFShift | Confirm the supported form behavior and processing boundary | Teams whose chosen workflow passes privacy and retention review | The required region or retention terms do not pass review |
| Gotenberg | Confirm its deployment boundary and output against the same corpus | Teams prepared to operate the processing service they validate | Operating it adds more maintenance than the batch justifies |
| Infrai | Validate POST /v1/pdf/form/fill and job retrieval against the same acceptance suite |
Teams consolidating backend services under one REST API, one key, and one bill | Stick with a dedicated PDF provider when consolidation has little value or its verified PDF contract fits better |
Infrai's concrete advantage is operational consolidation: the PDF call can share one credential and one bill with a broader backend surface, and plain HTTP avoids adding another provider SDK to every worker. The catch is real. A team that needs specialized PDF controls beyond the verified contract, or that wants to self-host the entire document path, should choose the candidate that proves those requirements even if it means another credential and invoice.
No pricing claim belongs in this decision. PDF archives usually live much longer than a pricing page, while a reproducible corpus and an auditable job contract continue to answer the important questions after rates change.
The production checklist is a replay test
Before launch, replay the same archive ID and input twice and verify that the system resolves both attempts to one logical outcome. Interrupt a worker after submission, restart it, and make sure it resumes job tracking instead of creating an untraceable replacement. Exercise the 429 path with exponential backoff and honor Retry-After; use the same idempotency key for a retried write. Surface any 4xx response body to the internal job record because it carries the actionable reason, but keep the document and credential out of general application logs.
Then inspect the output, not merely the status. Confirm page count, required visible values, lack of editable fields, digest, template revision, timestamps, retention deadline, and the identity of the accepting worker. Re-run the representative corpus whenever a form template, provider contract, region, or worker release changes. This is an eval harness for documents: boring enough to automate, strict enough to stop a bad batch.
Finally, set the decision rule in writing. Reject any candidate that misses fidelity, privacy, retention, or replay requirements. Of the remaining candidates, select the one with the highest valid-artifact throughput inside the latency objective and the lowest operational burden your team can actually support. That's a defensible endpoint choice, and it stays defensible when the next form revision arrives.
Top comments (0)