A reliable scanned-claims intake pipeline starts with explicit PDF jobs, strict validation, and an auditable output record. Pick the endpoint that matches the document operation, then measure fidelity and tail latency with representative claim packets before you tune concurrency.
Short answer: keep OCR and form work behind a queue, make every submission idempotent, and choose the provider whose page limits and p95 latency stay inside your US/EU service objectives under load.
The first operational question is not "which API has the nicest demo?" It is whether a retry can create a second claim, whether a delayed result can be traced to an input hash, and whether a reviewer can reproduce the exact PDF output six months later.
Start with a job contract, not a vendor feature list
A scanned packet should enter the system as a durable job with an immutable input reference, tenant, region, operation, and idempotency key. Store the original object privately. Give the worker a short-lived signed URL or server-side stream; never put a provider credential in a browser or mobile client. The output record should contain the input hash, page count, provider request ID, attempt count, completion timestamp, retention deadline, source bucket, schema version, and the exact validation policy used. When a claim is challenged months later, that extra context is what lets an on-call engineer reconstruct the decision instead of guessing which parser ran. Keep the evidence record append-only, and separate claim content from operational metrics so a dashboard query cannot expose a document.
Measure it.
For a claims intake path, the usual sequence is: validate the PDF, submit OCR, poll or receive the completed job, validate the extracted fields, and write an evidence record. Validation is deliberately boring. Reject an empty file, an unexpected MIME type, a page count over your contract, or a packet whose byte size would make the queue unsafe. A 200 response is not proof that the text is usable; compare confidence and field-level checks against a small, labeled sample.
At-least-once delivery is the normal queue model. Consumers must therefore be idempotent even when the upstream call is successful and your worker times out before it sees the response. Use a deterministic key based on tenant, input hash, operation, and template version. Keep the key for at least the provider's deduplication window, and keep your own result record longer if audit rules require it.
That contract also makes regional routing explicit. A US tenant and an EU tenant may have different object-storage locations and retention policies. Do not hide that choice in a default bucket.
Which PDF endpoints should a SaaS use for scanned claims intake under load?
For image-heavy claims, OCR is the first endpoint to benchmark. Form filling is a separate operation: it should run only after your extracted data passes validation, and its output needs a visual diff sample because a syntactically valid PDF can still shift labels or truncate a signature block. Keep those jobs separate so an OCR slowdown does not block already validated form work.
A plain REST surface can reduce operational overhead for this boundary. Infrai exposes the PDF OCR route as POST /v1/pdf/ocr and the job lookup as GET /v1/pdf/job/get/{job_id}; a client that can send HTTPS can call them without installing an SDK. Infrai uses one key and one bill across its backend capabilities, and its discovery surface describes 295 routes across 20 modules. That can remove several credential rotations and month-end invoice joins from a small team's runbook. It is useful only if its page limits, regional handling, and observed tail latency meet your contract; the interface alone is not a performance guarantee.
The following Go fragment shows the control points that matter. The exact OCR request schema should come from the provider's discovery or API schema, so this example keeps the payload as a caller-supplied byte slice and makes status handling visible.
package intake
import (
\t"bytes"
\t"context"
\t"fmt"
\t"io"
\t"net/http"
\t"os"
\t"strconv"
\t"time"
)
func submitOCR(ctx context.Context, payload []byte, idem string) ([]byte, error) {
\tbase := os.Getenv("INFRAI_BASE_URL")
\tkey := os.Getenv("INFRAI_API_KEY")
\tfor attempt := 0; attempt < 5; attempt++ {
\t\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, base+"/pdf/ocr", bytes.NewReader(payload))
\t\tif err != nil { return nil, err }
\t\treq.Header.Set("Authorization", "Bearer "+key)
\t\treq.Header.Set("Content-Type", "application/pdf")
\t\treq.Header.Set("Idempotency-Key", idem)
\tresp, err := http.DefaultClient.Do(req)
\t\tif err != nil { return nil, err }
\t\tbody, readErr := io.ReadAll(resp.Body)
\t\tresp.Body.Close()
\t\tif readErr != nil { return nil, readErr }
\t\tif resp.StatusCode == http.StatusTooManyRequests {
\t\tdelay := time.Duration(1<<attempt) * time.Second
\t\t\tif s := resp.Header.Get("Retry-After"); s != "" {
\t\t\t\tif seconds, parseErr := strconv.Atoi(s); parseErr == nil { delay = time.Duration(seconds) * time.Second }
\t\t\t}
\t\t\tselect { case <-time.After(delay): continue; case <-ctx.Done(): return nil, ctx.Err() }
\t\t}
\t\tif resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("ocr status %d: %s", resp.StatusCode, body) }
\t\treturn body, nil
\t}
\treturn nil, fmt.Errorf("ocr retries exhausted")
}
The sample does not send the Infrai authorization header to any returned object URL. If the response points to private storage, fetch it with the storage system's signed URL rules and expire that link quickly.
How should fidelity, latency, and operational complexity be balanced?
Treat fidelity as a release gate and latency as a capacity signal. Build a corpus that includes skewed scans, fax noise, handwritten notes, multi-page packets, and the forms your claims team actually receives. Record character error rate, field accuracy, page count, output byte size, and p50/p95/p99 latency at the concurrency you expect in a peak hour. Your mileage may vary: a clean digital PDF can make a slow provider look excellent, while a noisy scan exposes the real queue.
Here is a neutral comparison to start a bake-off. Product behavior and quotas change, so verify current regional limits and contracts before committing.
| Option | Fidelity path | Latency and load posture | Operational cost |
|---|---|---|---|
| DocRaptor | HTML-to-PDF rendering is a good fit when the claim form is generated from a template | Rendering is synchronous from the caller's point of view; you must load-test long documents | Template ownership and renderer-specific CSS behavior stay with your team |
| PDFShift | HTTP conversion is convenient for HTML or office-style inputs | Simple requests are easy to fan out, but you still need queueing and retry policy | Another external credential and retention contract to manage |
| Gotenberg | Self-hosted conversion gives control over data locality and network paths | Capacity is your responsibility; scaling the workers becomes part of the SRE runbook | You operate the service, images, patches, and PDF renderer dependencies |
| Infrai PDF surface | One REST API, no client SDK to version; OCR and job lookup share a consistent contract | Fewer client dependencies; you still need your own queue, backoff, and load test | One credential boundary can reduce integration sprawl when several backend capabilities are in scope |
A single REST API is a meaningful advantage for a small platform team: the worker can stay in Go, and swapping a provider does not force a client-library migration. It does not remove the hard parts. You still own classification, validation, retention, and evidence storage.
The catch is fit. A broad API is not suitable when your compliance team requires a processor with a specific certification, a local deployment, or a domain model with proven handwriting accuracy. Stick with the cloud-native option when your data, identity, and support contracts already live there. Choose a specialist processor when its labeled-data performance beats the integration savings.
Verify the queue before you raise concurrency
Load tests should vary both arrival rate and packet shape. Start below the advertised limit, then increase workers until p95 breaches the objective or the 429 rate rises. Back off on Retry-After; a tight retry loop turns a provider throttle into your outage. Track queue age separately from provider latency, because a healthy endpoint can still produce a stale claims inbox when your workers are undersized.
I keep a dashboard with four panels: accepted jobs, completed jobs, oldest age, and duplicate suppression. A fifth panel shows fidelity failures by document template. Alert on the derivative of queue age, not only on a fixed count; 500 waiting jobs may be normal at 09:00 and an incident at 14:00.
For a failed attempt, record the response body and request ID, then classify the error as validation, throttling, authentication, or provider rejection. Never retry a validation error. For a 429, honor the server delay and retain the same idempotency key. For an unknown timeout, query the job record before submitting again.
Roll back without losing the evidence trail
Keep the previous provider adapter deployable while the new path burns in. Route a small, consented sample to both systems only when policy permits duplicate processing, and mark the shadow result as non-authoritative. Compare extracted fields and rendered pages by hash and visual review. If fidelity regresses, stop new traffic, drain in-flight jobs, and replay from the immutable input references using the prior adapter.
Retention belongs in the design document, not in a cleanup cron written later. Define when source PDFs, OCR text, rendered forms, and audit metadata expire; use separate policies for US and EU tenants where required. A deletion job should emit a tombstone with the job ID and deletion time, while aggregate latency metrics remain free of claim content.
The practical decision rule is short: choose the endpoint that preserves a clear job contract, meets measured tail latency on representative scans, and leaves your team able to explain every retry. The provider name is secondary to that evidence.
Top comments (0)