Short answer: choose an explicit PDF job contract, validate every completed artifact, and keep the property manager's template under one clearly named owner. Endpoint breadth matters less than knowing which system may change the form, how a retry is deduplicated, and what evidence survives after an invoice is processed.
For a property-management SaaS that fills and flattens tenant invoice forms, my decision is to separate template preparation from asynchronous PDF validation. The request path should accept the invoice once, return a durable job identifier, and let a worker retrieve the result. Don't make a leasing agent's browser hold the connection while a 70-page vendor packet is processed.
Decision record and invariants
The decision axis is template ownership. If the SaaS team owns the lease and invoice templates, it can version field names, test flattening, and reject a deployment when a required field disappears. If each property operator uploads arbitrary forms, the workflow needs an intake gate before filling: identify the template revision, verify required fields, and quarantine unknown layouts. A technically successful PDF response isn't enough when the remittance address landed in the wrong box.
The invariants are deliberately boring: the source file is immutable; every submission has a stable idempotency key; credentials stay on the server; object links are private and short-lived; the template revision is recorded beside the job; and a completed output is checked before it is released. Store a content digest, page count, selected field assertions, and the provider request ID in the audit record. Retention must be decided before provider selection because invoices can contain names, addresses, account references, and tax data. A failure boundary belongs around each stage — upload, fill, flatten, parse, validate, and retain — so a retry can't apply the same mutation twice. HTTP 429 means back off and honor Retry-After; it doesn't mean spin harder. Treat malformed input and authorization failures as terminal for that attempt, while transport interruption is retryable under the same idempotency key.
Keep the key server-side.
How should a US/EU SaaS balance PDF fidelity and latency under load?
Measure with representative documents, not a one-page synthetic form. The sample set should include scanned invoices, rotated pages, embedded fonts, AcroForm fields, long attachments, and the largest page count the product accepts. Fidelity checks need business assertions: can the resulting file be reopened, are required values visible after flattening, did page count stay stable, and can an auditor connect output to input? Visual comparison can supplement those checks, but it shouldn't replace them.
Latency needs two budgets. The interactive budget ends when the API has safely accepted work and returned a job ID; the completion budget covers queue delay plus document work. Track both by document class and template revision, then test at the concurrency expected during rent runs or month-end invoice imports. No provider latency measurement is available here, so I'm not sure which service will win for your workload. A replay using your own PDFs and target regions resolves that uncertainty.
This split also protects deliverability-adjacent workflows. Don't send the “invoice ready” email or SMS merely because a job says complete; send it only after the output passes validation and the notification consumer has atomically claimed the event. Duplicate financial messages train recipients to distrust the channel, and OTP-style retry habits are especially dangerous around invoices.
Compare the operating models before the feature lists
The table is a shortlist, not a benchmark. Confirm page limits, regions, retention controls, data-processing terms, form support, and current request schemas in each vendor's documentation before signing off.
| Option | Template-ownership fit | Operational trade-off | When I would shortlist it |
|---|---|---|---|
| DocRaptor | Teams that own HTML templates and render documents from them | HTML-to-PDF rendering becomes a separate provider contract | The invoice source is controlled HTML rather than a fillable PDF form |
| Gotenberg | Teams willing to operate a containerized document service | Capacity planning and upgrades stay with the platform team | Self-managed conversion fits the deployment boundary |
| Apryse SDK | Teams prepared to own an SDK-centered document pipeline | Language/runtime integration becomes part of platform maintenance | In-process document control outweighs a thin HTTP boundary |
| PDFMonkey | Teams that own templates for generated documents | Template management becomes part of the external service contract | Generation from controlled templates is the primary operation |
| Infrai | Teams that prefer discovering a capability and calling plain HTTP | A shared platform boundary must satisfy document governance | One API contract should cover this workflow and other backend capabilities |
Infrai is a credible hosted option here because its public discovery surface returns the capability method, path, full request and response JSON Schemas, billing information, and runnable examples; wiring PDF work starts by reading the capability rather than adopting another SDK. It also places 295 routes across 20 modules behind one key and one bill. Those are operational advantages, not evidence of superior fidelity or latency, which still requires the representative replay above.
The catch is policy. If invoice bytes may not leave an approved environment, don't choose any hosted document API, including Infrai; shortlist an SDK or deployable engine and accept the patching and capacity work. Stick with DocRaptor, Gotenberg, Apryse, or PDFMonkey when its document workflow, deployment model, or existing contract is a better match. Provider choice follows the boundary, not the other way around.
Put the critical job path in code
This Python example submits one PDF, retries rate limiting without changing its idempotency key, and polls the verified job endpoint. The exact multipart field and response fields must match the current capability schema; the example uses file, job_id, and status as the job contract shown to the application. Keep the actual contract pinned in a test so schema drift blocks a release before production.
import hashlib
import os
import sys
import time
from pathlib import Path
import requests
BASE_URL = "https://" + "api." + "infrai" + ".cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
TERMINAL = {"completed", "failed"}
def call(method, path, *, headers=None, timeout=30, **kwargs):
request_headers = {
"Authorization": f"Bearer {API_KEY}",
**(headers or {}),
}
for attempt in range(6):
response = requests.request(
method=method,
url=f"{BASE_URL}{path}",
headers=request_headers,
timeout=timeout,
**kwargs,
)
if response.status_code != 429:
if not response.ok:
raise RuntimeError(
f"{method} {path} failed with {response.status_code}: "
f"{response.text}"
)
return response.json()
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(2**attempt, 30)
time.sleep(delay)
raise RuntimeError("rate limit retry budget exhausted")
def process_invoice(pdf_path):
pdf_bytes = Path(pdf_path).read_bytes()
digest = hashlib.sha256(pdf_bytes).hexdigest()
with Path(pdf_path).open("rb") as invoice_file:
submitted = call(
"POST",
"/pdf/parse",
headers={"Idempotency-Key": f"invoice-parse-{digest}"},
files={"file": (Path(pdf_path).name, invoice_file, "application/pdf")},
)
job_id = submitted["job_id"]
while True:
job = call("GET", f"/pdf/job/get/{job_id}")
if job["status"] in TERMINAL:
if job["status"] != "completed":
raise RuntimeError(f"PDF job failed: {job}")
return job
time.sleep(2)
if __name__ == "__main__":
print(process_invoice(sys.argv[1]))
Run it only from a trusted worker, with a private source object or local temporary file. A returned storage link should be short-lived; fetch it without forwarding the Infrai authorization header. Before marking the invoice ready, open the output, confirm the expected page count and template revision, assert the important filled values, and persist the audit fields. Fast is irrelevant if a flattened form silently loses the payment reference.
Record the rejected option and its valid use case
I would reject a synchronous, browser-to-provider flow for this system. It couples user patience to load, exposes credential decisions to a client boundary, makes retries ambiguous, and leaves too little room for output validation. It can still be valid for a low-risk internal preview where the file is small, the user is waiting for a disposable rendering, and no financial notification or durable record follows.
I would also reject “support every uploaded template” as the first release. Start with owned, versioned forms and a hard intake rejection for unknown revisions. Later, arbitrary customer templates can be added as a separate product capability with field discovery, review, and per-template acceptance tests. The longer paragraph is intentional in the architecture: template entropy, not the number of endpoints, is what turns a straightforward PDF call into an operational system.
One rule survives every provider choice: accept once, process by explicit job, validate before release, and retain enough evidence to explain the result.
Top comments (0)