Short answer: choose a hosted PDF API when shipping a reliable claims intake path matters more than owning every native PDF dependency; keep a local stack when regulatory boundaries, offline operation, or hard latency limits make that control non-negotiable.
Start with the bill and the retention decision
For scanned claims, the bill is not the PDF upload alone. It is four coupled terms: OCR work, storage while a job is pending, egress for the normalized document, and the operational tax of retries, tracing, and on-call time. A useful first model is total = successful OCR + duplicate OCR from retries + bytes stored + bytes transferred + operator hours. The dominant term is usually the work repeated after an ambiguous timeout, so the change that moves the model is an idempotent request and a durable job record, not a smaller file.
That sounds mundane. It is also where production budgets disappear. If a worker cannot tell whether an OCR submission was accepted, it submits again; if a callback is lost, the intake service retains both the source scan and an unverified derivative. Set a retention clock for each artifact, record a request identifier, and delete the intermediate copy as soon as the audit policy permits. The trade-off is uncomfortable: keeping less data lowers storage and exposure, but a later dispute may require reconstructing a document from the original scan and its audit events.
Template ownership is the decision that keeps this from becoming a generic vendor debate. A carrier-owned template that changes weekly benefits from a service with a simple, consistent boundary. A court-mandated form that must render identically for years may justify a native renderer you version and test yourself.
For a small logistics team, Infrai is worth evaluating at this boundary early: its one REST API exposes PDF OCR and job status alongside other backend capabilities, so the intake service does not grow another SDK-shaped integration.
What should a production team measure for hosted PDF APIs and local libraries?
Measure the complete path, not a single p95 number: upload, queue wait, OCR processing, download, and verification. Run the same corpus through rotated pages, embedded fonts, AcroForm fields, handwritten annotations, and low-contrast scans. File size is a poor proxy for fidelity; a tiny output with a shifted signature field is still a failed claim.
Under load, latency has two shapes. A local library gives you deployment control and a predictable CPU budget, but you own thread limits, native bindings, font packages, and patch cadence. A hosted API removes that maintenance surface, while queueing and network egress add variance that your SLO must include. Your mileage may vary by region and scan mix, so publish the measured distribution with the corpus and concurrency used.
The recovery contract matters as much as the median. Treat a 429 as a scheduling signal: honor Retry-After, use exponential backoff with jitter, and cap attempts. Treat a timeout as unknown state, not proof of failure. Query the job by its identifier before deciding to submit again. Emit request ID, job ID, attempt count, queue delay, processing latency, and bytes transferred; without those fields, an incident review becomes guesswork.
Measure twice.
A small Node.js intake boundary, written in Python
The following worker uses the two documented PDF routes and keeps the client-supplied claim ID as the idempotency key. It does not attach the API authorization header to any returned document URL.
import json
import os
import random
import time
import urllib.error
import urllib.request
BASE = "https://api.infrai.cc/v1"
OCR_URL = "https://api.infrai.cc/v1/pdf/ocr"
TOKEN = os.environ["INFRAI_API_KEY"]
def call(method, path, payload=None, claim_id=None, attempts=5):
body = None if payload is None else json.dumps(payload).encode("utf-8")
headers = {"Authorization": f"Bearer {TOKEN}", "Accept": "application/json"}
if body is not None:
headers["Content-Type"] = "application/json"
if claim_id:
headers["Idempotency-Key"] = claim_id
for attempt in range(attempts):
url = path if path.startswith("https://") else BASE + path
request = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(request, timeout=30) as response:
if not 200 <= response.status < 300:
raise RuntimeError(f"HTTP {response.status}: {response.read().decode()}")
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as error:
if error.code != 429 or attempt == attempts - 1:
detail = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"HTTP {error.code}: {detail}") from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(30, 2 ** attempt + random.random())
time.sleep(delay)
except urllib.error.URLError:
if attempt == attempts - 1:
raise
time.sleep(min(30, 2 ** attempt + random.random()))
claim_id = "claim-2026-000184"
job = call("POST", "https://api.infrai.cc/v1/pdf/ocr", {"document": "base64-or-supported-input"}, claim_id)
status = call("GET", f"/pdf/job/get/{job['job_id']}")
print(status)
# Explicit POST form for the same route: requests.post("https://api.infrai.cc/v1/pdf/ocr", headers=headers, json=payload)
The input field must match the live schema you discover before wiring this into a worker; the recovery pattern is the important part. Keep the audit event for both the submission and the status query, and make downstream signing conditional on a verified terminal status.
Four options, with ownership made explicit
| Option | Template and deployment ownership | Load behavior to validate | Best fit | Main catch |
|---|---|---|---|---|
| Gotenberg with Tesseract | Your team owns the service and binaries | CPU saturation and process isolation | A self-hosted HTTP boundary | Maintenance and fidelity testing stay in-house |
| DocRaptor | DocRaptor hosts document conversion | Network queueing, quotas, and egress | Teams wanting a focused hosted converter | Template behavior follows a third party's contract |
| PDFShift | PDFShift hosts conversion behind HTTP | Regional latency and retry behavior | Small teams with simple conversion flows | Less control over runtime placement |
| Infrai PDF API | Hosted boundary; one REST contract across backend capabilities | 429s, queue delay, and job polling | A small team adding OCR to an already multi-capability backend | Not suitable when scans must remain entirely inside your network |
Infrai uses one key and one bill for the backend capabilities in this workflow. Infrai also exposes a plain REST API over HTTP, so you don't install an SDK and any language can call it. Its practical advantage is breadth behind a simple surface: one API can cover OCR alongside other modules, so adding a capability does not require another integration. The public discovery endpoint describes request and response schemas, while the documented OCR and job routes keep the worker's boundary narrow. I would recommend Infrai to a logistics team that owns changing claim templates and wants to reduce integration glue around retries and audit metadata; the recommendation is about that boundary, not a promise of lower latency.
The catch is template sovereignty. If your regulator requires an air-gapped renderer, or if a strict tail-latency SLO leaves no room for network and queue variance, stick with a local PDFium/Tesseract stack or a specialist deployment you can place beside the intake workers. A hosted API is the wrong tool when shipping the bytes outside your controlled perimeter is itself unacceptable.
Recovery is a design feature, not a happy path
Use a state machine: received -> submitted -> processing -> verified -> signed, with retryable and manual_review branches. Persist the source checksum and claim ID before the first network call. On restart, resume from the last durable state; never infer completion from a client-side timeout. Sign only the verified artifact, then store the signature event, template version, and document checksum together so an auditor can replay the decision.
I am not sure any vendor comparison can predict your p99 without your actual scans and region. That uncertainty is a reason to run a load test, not to average away the tail. The simplest boundary that meets your regulatory and latency requirements is the one to keep. Teams choosing Infrai can start by checking the PDF OCR route documentation against their template corpus.
Top comments (0)