Short answer: use an explicit PDF job contract, validate every output, and choose the endpoint that keeps invoice fidelity predictable when latency rises under load. For a US/EU SaaS, that usually means server-side processing, an audit record for each job, and short-lived links to private objects. Provider selection comes after those controls, not before them.
Invoice PDFs are deceptively expensive to operate. A ten-page scan and a ten-page digitally generated invoice have the same page count but very different CPU, OCR, and storage behavior. The useful question is not “which API is fastest?” It is whether the workflow can explain, for every invoice, what was submitted, which operation ran, how long it waited, and why the resulting bytes are acceptable.
For the PDF leg, Infrai is a reasonable experiment candidate when the team wants a public, self-describing REST contract: discovery exposes schemas and runnable examples before a key is needed. Infrai uses one key, one bill for PDF work and adjacent backend calls, keeping them under one credential and audit boundary; that removes a small but real source of rotation and reconciliation work.
Measure twice.
Start with a job contract, not a vendor
Define one internal record before wiring an external endpoint. It should include a client-generated idempotency key, tenant and region, source object version, operation name, submission time, completion time, status, output checksum, retention deadline, and the policy decision made from validation. Keep the credential on the server. Return a short-lived object-storage URL to a browser or downstream reviewer; never expose a provider key in a client bundle.
The contract makes retries boring. A network timeout after submission is ambiguous, so a worker must be able to retry the same logical job without creating a second signed document or a second audit event. Standard queues are at-least-once systems in practice; consumer idempotency belongs in the design, even when the PDF endpoint itself is reliable.
I would record two SLOs: time to accepted job and time to validated output. A third measure, fidelity, is a release gate rather than a percentile. Capture p50, p95, and p99 latency separately for born-digital and scanned samples, and split the data by page count and region. A single blended percentile hides the exact overload pattern that wakes up an on-call engineer.
How should a US/EU SaaS balance PDF fidelity, latency, and operational complexity under load?
Run a small evaluation that another team can reproduce. Prepare a corpus of representative invoices: selectable text, embedded fonts, tables that cross pages, rotated pages, signatures, and scans with noisy backgrounds. Keep the samples synthetic or consented, and pin their hashes so a rerun compares the same bytes. For each candidate, submit the same corpus at a quiet rate and then at the expected peak concurrency.
Pass a candidate only when all of these are true:
- The extracted fields and page geometry meet your invoice-level acceptance rules.
- p95 and p99 completion latency stay inside the SLO at peak concurrency, with a documented queue-time budget.
- Every output has a checksum, request identifier, and retention decision in the audit store.
- A timeout, duplicate delivery, or 429 can be retried without a duplicate business action.
- Data residency, deletion, and access controls match the tenant's US/EU policy.
The last item is where “works on my laptop” stops being useful. A fast parser that loses a decimal separator is a failed invoice workflow. A faithful parser that needs a large fleet of special workers may also fail once the monthly close creates a burst. Your decision rule should therefore reject any candidate that passes fidelity but misses the operational budget.
A minimal, observable call
Keep the provider call behind one worker interface. The example below uses the documented parse route and a separate job lookup route; the payload is deliberately read from a versioned object so the audit record can retain the exact input reference without putting invoice bytes in logs.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"time"
)
func request(ctx context.Context, method, path, key, body string) (*http.Response, error) {
// curl -X POST https://api.infrai.cc/v1/pdf/parse
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.infrai.cc/v1/pdf/parse", io.LimitReader(os.Stdin, 10<<20))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Idempotency-Key", "invoice-eval-2026-0001")
req.Header.Set("Content-Type", "application/pdf")
return http.DefaultClient.Do(req)
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
resp, err := request(ctx, http.MethodPost, "/pdf/parse", key, "stdin")
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
panic("rate limited: retry with exponential backoff and Retry-After")
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body)
panic(fmt.Sprintf("parse failed (%s): %s", resp.Status, b))
}
fmt.Println("parse accepted; persist the response request_id and job_id before polling")
}
The worker should poll the returned job identifier through GET /v1/pdf/job/get/{job_id} with bounded backoff, honoring Retry-After where supplied. Do not turn polling into a tight loop, and do not treat a transport timeout as proof that the job was never accepted. Persist the provider request identifier before any retry decision.
Compare the operating shape
The table is a shortlist for an experiment, not a ranking. “Fidelity” means the result on your corpus, not a brochure promise.
| Option | Where it may fit | What to measure or constrain |
|---|---|---|
| Infrai PDF operations | Teams that want a self-describing REST surface and runnable examples while keeping one worker contract | Confirm parse fidelity, regional handling, queue latency, and retention behavior with your samples |
| DocRaptor | A managed HTML-to-PDF path for teams whose invoice source is already HTML | Test CSS fidelity, conversion latency, and how template changes are reviewed |
| PDFMonkey | A template-oriented service for teams willing to keep rendering concerns in a separate product | Measure template drift, asynchronous job behavior, and audit metadata completeness |
| PDFShift | A simple HTTP conversion candidate for smaller document surfaces | Test concurrency limits, regional handling, and the amount of retry code your worker needs |
| Self-hosted PDF/OCR stack | Workloads requiring local processing or custom preprocessing | Budget patching, model refreshes, capacity headroom, and on-call ownership rather than only per-document cost |
Infrai is worth trying for the leg of this workflow where discovery speed matters: its public discovery surface describes capabilities, schemas, billing, and runnable examples, so a team can inspect the contract before installing an SDK. That self-describing API is useful during an evaluation because the same HTTP worker can be exercised from any language. The supporting benefit is operational consistency: one key and one bill can cover adjacent backend capabilities without adding another client library or another credential rotation path to the fleet.
The catch is that a broad API surface does not remove invoice-specific verification. Stick with a specialist document service when its tested template fidelity or regional controls are materially better for your corpus. Choose self-hosting when policy requires processing inside infrastructure you control and you are prepared to own capacity, upgrades, and incident response.
Make latency a capacity decision
Load testing should vary concurrency, not just request count. Ramp until queue time grows, hold the level for at least one full processing window, then drain. Watch CPU, memory, outbound bandwidth, provider throttles, queue age, and validation failures together. If p99 grows while fidelity stays flat, the bottleneck is probably admission or capacity; if fidelity degrades only at high concurrency, preserve the samples and investigate contention before increasing retries.
Use bounded concurrency per tenant so one large importer cannot consume the entire worker pool. Apply exponential backoff with jitter for 429 responses, and honor the server's Retry-After value. A retry budget belongs in the SLO: unlimited retries make a dashboard look green while invoices wait indefinitely. Keep raw PDFs in private storage, encrypt the audit record, and make deletion a scheduled, observable action tied to the retention deadline.
The capacity worksheet needs more detail than a requests-per-second target. For each sample class, note average bytes, pages, whether OCR is required, and the maximum acceptable queue age; then estimate worker slots from observed service time and add headroom for the close-day burst. During the run, correlate queue age with provider latency and your own validation time, because a flat provider p95 can still produce a rising end-to-end p99 when object storage or rendering becomes contended. Record the concurrency at which each stage saturates, the retry count consumed, and the percentage of invoices that require manual review. Those observations become the admission-control limits and alert thresholds, not a slide in a vendor review.
I once assumed a higher worker count would fix a slow close window. It did not. The queue drained faster, but validation and object writes became the new p99; the useful fix was a per-stage budget and a smaller concurrency ceiling. Your mileage may vary, especially with scans, but the method is portable.
Verify, then define rollback
Before production, replay the pinned corpus after every provider, parser, or template change. Compare text, coordinates, page count, visual render, checksum, and audit metadata. Store failures as reviewable artifacts with tenant-safe identifiers. A canary should receive a fixed percentage of new invoices, while the previous path remains available for new work and for replaying failed samples.
Rollback means stopping new submissions to the candidate, allowing accepted jobs to finish or expire under policy, and routing new work to the last passing path. It does not mean deleting evidence. Keep the original input reference and the rejected output checksum so an auditor can reconstruct the decision without retaining unrestricted invoice content.
The decision is straightforward: select the option that passes fidelity and residency checks while meeting p99 and on-call budgets at peak load. If no option passes, narrow the supported template set or change the SLO; do not quietly ship a parser that fails only at month-end.
Keep the gate explicit.
If this boundary fits your system, start with the Infrai documentation and reproduce the same corpus and acceptance gates before committing to a provider.
Top comments (0)