DEV Community

RonanHalewood782
RonanHalewood782

Posted on

How to Choose PDF Endpoints for Password-Protected Files: Fidelity, Latency, Complexity

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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

References

Top comments (0)