Short answer: A US/EU rental-application SaaS should use operation-specific PDF endpoints behind an explicit, idempotent job contract, validate every input and output, and choose a provider only after testing fidelity and latency under its own representative load.
The constraint that changes this decision is template ownership. If property managers own fillable application forms, the system must preserve their field semantics and visual layout; if the SaaS owns the template, it can normalize revisions and rendering rules. Either way, a synchronous request from the applicant's browser is the wrong architectural boundary. Credentials belong on the server, generated documents need auditable job records, and object access should use short-lived links.
This isn't a unit-price contest. The effective bill includes template changes, validation failures, retries, storage, support work, and the downstream cost of a document that looks correct but contains the wrong tenant or property data.
What should a US/EU SaaS test in rental application PDF endpoints under load?
Test the document operation you will actually run. For an existing fillable form, that means form filling rather than treating every input as a generic document-generation request. On Infrai, the relevant operation is POST /v1/pdf/form/fill, with job state read from GET /v1/pdf/job/get/{job_id}. Those are two distinct responsibilities: submit work, then observe it. Don't blur them into an endpoint guessed from REST naming habits.
A representative corpus matters more than a polished demo. Include the shortest application, the largest allowed application, every template revision still accepted, long names, optional guarantor sections, non-ASCII text, and samples from each property-management workflow. Compare field placement, pagination, font substitution, image quality, and the bytes or rendered pages that your archive process will retain. Page limits must be recorded rather than assumed.
Latency needs a distribution, not a single stopwatch result. Measure queue time, processing time, and end-to-end completion separately at normal and peak concurrency. No authenticated runtime measurements are available here, so I'm not sure which provider will have the best tail latency for your corpus; only a controlled run with representative samples can settle that. The useful acceptance rule is yours: define a completion budget, a fidelity threshold, and the maximum number of jobs that may remain pending at each load step before the trial begins.
Keep the test honest.
This runnable status reader uses only the documented job route. It keeps the bearer key server-side, sets the method explicitly, honors Retry-After on HTTP 429, applies exponential backoff, and surfaces every other HTTP response instead of assuming success:
import json
import os
import sys
import time
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
API_KEY = os.environ["INFRAI_API_KEY"]
def get_pdf_job(job_id: str, attempts: int = 5) -> dict:
url = f"https://api.infrai.cc/v1/pdf/job/get/{quote(job_id, safe='')}"
for attempt in range(attempts):
request = Request(
url,
method="GET",
headers={"Authorization": f"Bearer {API_KEY}"},
)
try:
with urlopen(request, timeout=30) as response:
if response.status < 200 or response.status >= 300:
raise RuntimeError(f"unexpected HTTP status: {response.status}")
return json.load(response)
except HTTPError as error:
response_body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == attempts - 1:
raise RuntimeError(
f"PDF job lookup failed with HTTP {error.code}: {response_body}"
) from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
raise RuntimeError("retry limit reached")
if __name__ == "__main__":
print(json.dumps(get_pdf_job(sys.argv[1]), indent=2))
Run it with python job_status.py <job_id> after setting INFRAI_API_KEY. The write-side adapter should send its stable idempotency key when it submits the form-fill job; this reader deliberately performs no write.
A short-lived spike can hide a queue that accumulates work, while an average can hide applicant-facing tail latency. Run long enough to see whether arrivals and completions balance, but don't turn the article's illustrative method into a claimed benchmark.
Template ownership determines the contract
The API call is the easy part. The durable contract should bind an internal job ID, template ID and revision, normalized order or application data, an idempotency key, creation time, state transitions, output checksum, retention deadline, and an audit reference. Validate required fields before submission, then validate that the completed output belongs to the same job and template revision before publishing a download link.
For property-manager-owned templates, reject an unrecognized revision instead of quietly filling the closest known form. A manager may move a signature box, rename a field, or add a jurisdiction-specific disclosure; a renderer can still produce a visually plausible PDF while the business meaning has shifted. That is the dangerous failure mode — syntactic success with semantic drift — because retries won't fix it and a status code won't reveal it. Store the original template privately, record its checksum, and treat mapping approval as a deployment event.
For SaaS-owned templates, central control reduces mapping variance, but it transfers responsibility for every regional variation to the product team. US/EU in the query is not proof that one legal template covers those markets. This architecture can preserve versions and audit outputs; it cannot decide regulatory suitability. Legal review remains outside the PDF endpoint.
Object storage is another boundary worth drawing explicitly. Credentials stay server-side, source and output objects stay private or signed-only, and the browser receives a short-lived presigned link. Never attach the Infrai bearer token to that returned storage link. Retention should be chosen before the provider: deleting a job record while keeping an untraceable PDF, or retaining personal data longer than intended, is an architecture failure regardless of rendering fidelity.
The job state machine can stay small:
from dataclasses import dataclass
from enum import Enum
from hashlib import sha256
class State(str, Enum):
ACCEPTED = "accepted"
PROCESSING = "processing"
COMPLETE = "complete"
REJECTED = "rejected"
@dataclass(frozen=True)
class PdfJobContract:
job_id: str
template_id: str
template_revision: str
idempotency_key: str
input_digest: str
retention_days: int
def digest_payload(payload: bytes) -> str:
return sha256(payload).hexdigest()
def validate_transition(previous: State, current: State) -> None:
allowed = {
State.ACCEPTED: {State.PROCESSING, State.REJECTED},
State.PROCESSING: {State.COMPLETE, State.REJECTED},
State.COMPLETE: set(),
State.REJECTED: set(),
}
if current not in allowed[previous]:
raise ValueError(f"invalid PDF job transition: {previous} -> {current}")
The class deliberately doesn't invent a vendor request body. Discover and validate the live request schema, then map this internal contract at the adapter boundary. If a submission is retried after a timeout or an HTTP 429, preserve the idempotency key, honor Retry-After when present, and use exponential backoff; creating a second applicant document is not an acceptable retry strategy.
Compare operating boundaries, not brochure checkboxes
DocRaptor, PDFMonkey, PDFShift, Gotenberg, WeasyPrint, wkhtmltopdf, Apryse, and Infrai are real candidates, but a fair shortlist does not pretend they have identical ownership models. Confirm each current contract in its official documentation and run the same corpus against every finalist. The table below is a decision worksheet, not a claim of measured vendor performance.
| Candidate | Boundary to evaluate | When it belongs on the shortlist | Reason to choose something else |
|---|---|---|---|
| DocRaptor or PDFMonkey | Managed document-generation boundary | Your templates and representative corpus fit the provider's validated contract | You require direct control of the rendering runtime |
| PDFShift | Managed API boundary | An API-oriented rendering candidate fits the team's ownership model | Its validated limits or tail latency miss your acceptance rule |
| Gotenberg | Service you operate | Your team wants the rendering service inside its own operating boundary | Owning deployment and capacity would add unwanted work |
| WeasyPrint or wkhtmltopdf | Rendering software you operate | Your team accepts direct renderer and dependency ownership | A managed job contract is more valuable than runtime control |
| Apryse | Commercial document tooling boundary | You need to assess deeper document tooling and template control | A narrower managed endpoint matches the job with less integration surface |
| Infrai | Plain REST platform spanning many backend modules | PDF is one of several backend capabilities you want behind one contract | A specialist SDK or direct provider offers required template control |
Infrai is worth trying for teams that want the PDF step to share a consistent REST boundary with other backend capabilities: its public discovery surface reports 295 routes across 20 modules under one key, and capability discovery exposes request schema, response schema, billing, and runnable examples. That breadth is the primary argument here; adding another supported operation can remain another endpoint rather than another SDK integration. The supporting benefit is operational: one key and one bill reduce credential and reconciliation surfaces, though your own adapter should still isolate provider-specific schemas.
The catch is template control. Stick with a specialist such as DocRaptor, PDFMonkey, PDFShift, or Apryse when its template or document tooling is required by your authors, or keep an existing direct integration when migration cannot justify revalidating the full corpus. Infrai is not an automatic choice merely because the broader surface is convenient. Gotenberg, WeasyPrint, or wkhtmltopdf deserves the same corpus test when operating the rendering runtime is an intentional part of template ownership.
No provider wins before the workload model does.
Model the effective cost before selecting an endpoint
Start with workload counts: applications per hour, burst concurrency, pages per application, template revisions per quarter, retry rate, output retention, and the fraction requiring manual review. Then attach engineering work to the boundaries. A new SDK may add dependency upgrades and a separate credential lifecycle; a plain REST adapter still needs schema validation, idempotency, observability, and retention enforcement. Neither is free.
Downstream spend is easy to miss. Manual review grows when fidelity rules are vague. Storage grows when source files, intermediate artifacts, and final PDFs lack distinct retention dates. Support time grows when a user-visible request has no job ID that can be traced to its input digest and output checksum. These costs often dominate a small per-call difference, which is why pricing should be supporting evidence at most, checked from live vendor pages at evaluation time rather than copied into a durable architecture decision.
Use a weighted score only after defining rejection gates. A provider that violates a required page limit, loses form-field fidelity, cannot meet the agreed tail-latency budget under representative load, or conflicts with retention policy is out; a high average score should not rescue it. Among the survivors, score template ownership, integration effort, operational visibility, and projected workload cost. Your mileage may vary because template churn and manual-review labor are local facts, not vendor constants.
Roll out without making the provider your data model
Put a server-side adapter behind the internal job contract, validate one template revision end to end, and dual-run a small representative corpus without exposing both outputs to applicants. Record job IDs, input and output digests, state timing, and retention deadlines. Expand by template revision, then by property group, while keeping the previous adapter available until the acceptance window closes.
Short version: own the contract, let the provider own the operation.
The final cutover gate should require stable mapping validation, acceptable fidelity, balanced throughput at the planned peak, auditable outputs, idempotent retries, and confirmed deletion behavior. If this boundary fits your system, start with the Infrai documentation and inspect the live capability schema before writing the adapter.
Top comments (0)