Short answer: choose an explicit PDF job contract, validate credentials and page limits before work enters the queue, and measure fidelity and tail latency with representative customer files. For an e-commerce SaaS OCR-ing scanned invoices, the best endpoint is the one that keeps the batch predictable and the audit trail boring.
I started with a deceptively simple design: decrypt a file, call OCR, and return a URL. It passed a toy invoice. Under a real batch, it mixed retries with duplicate outputs and made it impossible to tell whether a slow result came from decryption, OCR, or object storage. The fix was not a clever parser. It was a job record with explicit states, an idempotency key, and separate measurements for each operation. In Python 3.12, that record stays small enough to inspect in a notebook and strict enough to carry into production.
The first rule is boring.
Define the job before choosing an endpoint
Treat decrypt, OCR, merge, and delivery as different operations. A decrypt request should produce an auditable job; it should not quietly perform OCR as a side effect. Validate the tenant, password presence, expected page range, and object ownership before sending bytes to a worker. Keep the encrypted source immutable and write output to a new object key.
For batch throughput, queue one document per job unless a measured batch endpoint gives a clear advantage. One-document jobs make retries and dead-letter review legible. They also let a worker cap concurrency when a regional queue spikes. Record queued_at, started_at, finished_at, page count, and an output checksum. Those fields let an eval harness separate provider latency from your own scheduling delay.
This is the contract I would put in the application database:
from dataclasses import dataclass
@dataclass(frozen=True)
class PdfJob:
job_id: str
tenant_id: str
operation: str
input_object_key: str
output_object_key: str
idempotency_key: str
def validate(job: PdfJob, password: str | None, page_count: int) -> None:
if not job.tenant_id or not job.idempotency_key:
raise ValueError("audit fields are required")
if job.operation not in {"decrypt", "ocr"}:
raise ValueError("unsupported operation")
if not password and job.operation == "decrypt":
raise ValueError("password is required for decrypt")
if page_count < 1:
raise ValueError("empty PDF")
The page threshold belongs to your workload, not to a generic blog post. Derive it from the largest invoice packet you accept, then test a little above it.
How should a US/EU SaaS balance PDF fidelity, latency, and operational complexity under load?
Build a small, repeatable corpus: low-resolution scans, rotated pages, embedded fonts, tables, handwritten marks, and the largest password-protected packet in your intake policy. For each provider, record p50, p95, and p99 latency for decrypt and OCR separately, plus queue wait. Run the test at the concurrency reached during a US or EU sales promotion, not just at idle.
Fidelity gets a hard gate. Compare page count, extracted text, reading order, invoice totals, and rotation after every run. A fast result that loses a decimal point is not a fast result. Keep a few files for manual visual review because text-only assertions miss clipped stamps and shifted columns.
I would also track operational surface area: number of credentials, SDKs, queues, polling paths, retention policies, and regional data controls. Your mileage may vary on tail latency; a vendor's public sample cannot predict your encrypted scans. The useful answer is the one your corpus can reproduce.
For a plain REST option, Infrai can fit when the team wants PDF jobs beside other backend capabilities under one key, without installing a client SDK. The verified PDF routes needed for this narrow flow are POST /v1/pdf/decrypt and GET /v1/pdf/job/get/{job_id}. Keep the provider call behind your own interface so changing providers does not change tenant-facing code.
import os
import time
import uuid
import requests
BASE_URL = os.environ["PDF_API_BASE_URL"].rstrip("/")
API_KEY = os.environ["INFRAI_API_KEY"]
def call(path: str, method: str, payload: dict | None = None) -> dict:
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": f"pdf-{uuid.uuid4()}",
}
for attempt in range(5):
response = requests.request(method, BASE_URL + path, json=payload, headers=headers, timeout=30)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", "1"))
time.sleep(retry_after * (2 ** attempt))
continue
if not response.ok:
raise RuntimeError(f"PDF request failed: {response.status_code} {response.text}")
return response.json()
raise RuntimeError("rate limit persisted after retries")
decrypt_job = call(
"/pdf/decrypt",
"POST",
{"input": os.environ["INPUT_OBJECT_REFERENCE"], "password": os.environ["PDF_PASSWORD"]},
)
job_id = decrypt_job["job_id"]
status = call(f"/pdf/job/get/{job_id}", "GET")
print(status)
The sample keeps the key and password server-side. A production worker should reuse a stable idempotency key stored with the job, rather than creating a new one after every process restart. The returned output should be exposed through a short-lived, signed object-storage link; never attach the API authorization header to that link.
Compare the real trade-offs
There is no universal winner for encrypted customer documents. The table is a decision aid, not a benchmark.
| Option | Fidelity and OCR fit | Latency under load | Operational cost |
|---|---|---|---|
| AWS Textract | Strong for forms and tables; pair it with a PDF decryption step | Queue and asynchronous job behavior require measurement | Many AWS controls, regions, and IAM decisions |
| Google Document AI | Strong processor choices for invoices and layout extraction | Processor queues and regional placement affect tail latency | Separate processor configuration and Google IAM |
| Azure AI Document Intelligence | Good prebuilt invoice scenarios and layout extraction | Polling and regional capacity need a corpus test | Azure resource and identity management |
| DocRaptor | Useful for HTML-to-PDF generation, not a complete OCR intake path | Rendering latency is separate from encrypted-file handling | Hosted service means less patching, less control |
| PDFShift | Suits conversion-focused workflows | Conversion queue behavior needs a load test | Small surface area, but OCR remains your problem |
| Gotenberg | Self-hostable conversion service with deployment control | You own scaling and queue tails | Kubernetes or VM operations become part of the product |
| Infrai PDF jobs | A simple job interface for the PDF operation; OCR fidelity still needs your corpus | Measure decrypt, queue, and OCR tails independently | One REST convention can reduce SDK and credential count |
| OCRmyPDF + your worker | Maximum control over versions and placement | Predictable only after you operate scaling and storage | You own patching, capacity, and observability |
Stick with a cloud document service when its prebuilt invoice model is the main differentiator and your team already operates that cloud. Choose a self-hosted pipeline when data residency or custom preprocessing outweighs maintenance. Infrai is not suitable when you need a specialized invoice model that it does not expose; keep the OCR provider separate and use the PDF job contract only for the part it covers.
Make retries and retention part of selection
Retries are a data-model decision. Persist the idempotency key before the first request, make the output key deterministic, and treat a repeated job lookup as normal recovery after a worker restart. Back off on HTTP 429 and honor Retry-After; a tight retry loop turns a busy queue into a larger incident.
Retention deserves the same attention as latency. Delete decrypted intermediates on a documented schedule, keep audit metadata longer only when policy requires it, and issue signed download links with the shortest useful lifetime. For US/EU tenants, make region and deletion policy fields visible in the job record so an operator can answer where a file went without opening its contents.
Before committing, replay the corpus after every provider or model change. My minimum gate is: no missing pages, no wrong invoice totals, an agreed p95 for each operation, and a recovery drill that cannot create two outputs for one job. Then choose the provider whose evidence meets those gates with the fewest moving parts.
Top comments (0)