Short answer: use explicit PDF jobs, validate every input and output, and make the signed result an auditable artifact. For most US/EU SaaS teams, start with a synchronous parse for small invoices, then move larger or slower documents to a job contract that can be polled and retried idempotently. Measure fidelity and latency with your own invoice samples before committing to a provider.
The workflow is deliberately plain: receive a bundle, validate its size and page count, split or merge according to the invoice policy, parse the resulting PDF, and store a signed output plus an audit record. A request ID follows the document through each stage. That ID matters more than a dashboard screenshot when finance asks why page 7 changed.
A small Python contract for invoice jobs
Keep credentials on the server. The browser gets a short-lived object-storage link, never a provider key. The worker owns retries, idempotency, and retention. This boundary also makes a notebook prototype portable: the same validation function can run in a batch job after it has graduated to production.
Here is a minimal client for the two verified PDF paths. It treats the request body as an already validated PDF byte stream and leaves the service-specific payload contract in one place, so changing providers does not spread assumptions through invoice code.
import hashlib
import os
import time
from dataclasses import dataclass
from typing import Any
import requests
@dataclass
class PdfClient:
base_url: str
api_key: str
timeout_seconds: float = 20.0
def _request(self, method: str, path: str, **kwargs: Any) -> requests.Response:
headers = kwargs.pop("headers", {})
headers["Authorization"] = f"Bearer {self.api_key}"
headers["X-Request-Id"] = kwargs.pop("request_id")
for attempt in range(4):
response = requests.request(
method, f"{self.base_url}{path}", headers=headers,
timeout=self.timeout_seconds, **kwargs
)
if response.status_code != 429:
if not 200 <= response.status_code < 300:
raise RuntimeError(f"PDF request failed ({response.status_code}): {response.text}")
return response
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2 ** attempt
time.sleep(delay)
raise RuntimeError("PDF request stayed rate-limited after four attempts")
def parse(self, pdf_bytes: bytes, request_id: str) -> dict[str, Any]:
digest = hashlib.sha256(pdf_bytes).hexdigest()
response = self._request(
"POST", "/v1/pdf/parse", data=pdf_bytes,
request_id=request_id, headers={"Content-Type": "application/pdf"}
)
result = response.json()
result["input_sha256"] = digest
return result
def get_job(self, job_id: str, request_id: str) -> dict[str, Any]:
return self._request(
"GET", f"/v1/pdf/job/get/{job_id}", request_id=request_id
).json()
client = PdfClient(
base_url=os.environ["PDF_API_BASE"],
api_key=os.environ["INFRAI_API_KEY"],
)
The example checks status codes, honors Retry-After, and uses a deterministic input digest for audit correlation. A write operation should also send a client-generated idempotency key; for a merge or split worker, persist that key with the invoice record before the first attempt. Standard queues are at-least-once, so a duplicate delivery must be harmless. Do not put the key in a browser bundle.
The parser response is not accepted blindly. Record the source digest, page count, extracted invoice number, and a hash of the produced artifact. If a field is missing or a page count changes unexpectedly, quarantine the document and keep the original bytes. That is a validation decision, not a retry decision.
How should a US/EU SaaS balance PDF fidelity, latency, and operational complexity under load?
Think in three budgets. Fidelity is whether text, tables, signatures, and page geometry survive the operation. Latency is the time from upload to a usable, verified artifact, including queue wait. Operational complexity is the number of moving parts your team must secure, observe, and retain.
Synchronous parsing is attractive for a two-page invoice because the caller gets a direct answer and fewer state transitions. It becomes a poor fit when PDFs are large, OCR is involved, or traffic arrives in bursts: a request timeout can hide work that is still consuming capacity. An explicit job gives you a durable state machine (accepted, running, verified, failed) and a place to apply backoff, but it adds polling, retention, and reconciliation code.
Load testing must use representative samples, not a generated blank page. Keep a fixture set containing scanned invoices, embedded fonts, rotated pages, tables, and a signed bundle. Run it at the expected concurrency and at a deliberately higher burst. Capture p50, p95, and p99 end-to-end latency, queue wait separately, page-limit rejections, and a fidelity score based on fields your accounting system actually uses. I initially expected average latency to decide the endpoint; under load, the long tail and retry amplification mattered more.
Measure twice.
For example, suppose a 40-page bundle contains three invoices and one signature page. A synchronous call may return quickly at low concurrency, then push p99 beyond your API gateway timeout when ten tenants upload at once. A job queue absorbs the burst, but the visible latency now includes queue wait and polling. If the worker retries after a timeout without an idempotency key, the same signature action can be applied twice even though the caller saw only one request. Record each state transition with the invoice ID, input hash, attempt number, and provider request ID; compare the completed artifact byte-for-byte with the expected fixture, then inspect the extracted totals and page geometry. This is the kind of test that exposes a fidelity regression that a median latency chart hides, and it gives finance an audit explanation instead of a vague “the service was slow.”
Three controls keep that tail visible. Bound client timeouts and poll intervals. Put a cap on attempts and send exhausted jobs to a review queue. Finally, retain the original, parsed output, and audit event for the period your legal and finance owners approve; delete temporary links sooner. I'm not sure a universal retention number exists, because regional policy and contract terms change the answer.
Comparing practical provider choices
The right comparison is a contract test, not a feature-count contest. Feed the same fixtures to each candidate, inspect signatures and table coordinates, and record how much infrastructure remains yours to operate.
| Option | Fidelity and workflow fit | Latency under load | Operational trade-off |
|---|---|---|---|
| docraptor | Practical HTML-to-PDF path when templates are the source; add your own parsing and audit ledger | Measure render time and burst behavior with signed fixtures | Hosted rendering is simple, but merge/split policy remains yours |
| pdfmonkey | Template-oriented generation for predictable invoice layouts | Check queue wait and webhook delivery under load | Less suitable for arbitrary scanned bundles |
| pdfshift | API-first conversion when input starts as HTML or a URL you control | Measure conversion tail latency and retry semantics | You still need storage, parsing, and signature verification |
| AWS Textract | Strong OCR and invoice analysis; pair it with your own merge, split, and signature ledger | Queue and regional behavior need measurement | Several AWS services and IAM policies to run |
| Google Document AI | Good processor specialization and human-review paths | Batch processors can trade freshness for throughput | Processor versions and Google Cloud controls add coordination |
| Azure AI Document Intelligence | Useful prebuilt invoice extraction and layout signals | Measure throttling and region selection with your fixtures | Azure resource and identity configuration stays in your estate |
| Infrai | One REST API and one key/bill across backend capabilities; explicit PDF paths keep the contract small | You still need your own load test and queue policy | Fewer integration surfaces, but retention, audit storage, and validation remain your responsibility |
This unified REST option is not a universal answer. It is not suitable when policy requires a hyperscaler-native processor, a specific regional control, or a feature outside its documented PDF capability. Stick with the provider that passes your signature and audit tests when those constraints dominate; a unified key is useful only after fidelity is acceptable.
Before selecting, write the operational checklist as part of the design: where the original PDF lives, who can read it, how links expire, which idempotency key maps to an invoice, what event proves verification, and which owner can delete retained artifacts. Then run a replay of the same bundle twice and confirm one business result, one audit trail, and no duplicate signature action. That replay catches more than a green health check.
Top comments (0)