For a US/EU SaaS, the right PDF endpoints for shipping labels are explicit jobs with validation and retention controls: a clipped barcode can stop a parcel, while an address retained past its purpose can become a privacy incident.
Short answer: use explicit PDF jobs, validate every artifact, and make idempotency, retention, and audit evidence part of the contract before comparing providers. Optimize fidelity first; measure latency and render cost with representative labels.
That rule also works for a customer-support system that signs contracts server-side. A return label and a signed authorization are both PDFs, but they do not have the same evidence or deletion requirements.
How should a US/EU SaaS choose PDF endpoints for shipping labels, fidelity, latency, and privacy?
Start with the document operation. Generation is different from signing, verification, compression, or rotation, so application code should name the operation rather than send a generic PDF request with an opaque options map. The name becomes an authorization boundary and an audit event. It also prevents a later template change from silently changing the meaning of a retry.
The provider-neutral job contract I use has document_id, operation, source_digest, template_version, attempt, requested_at, and a caller-generated idempotency key. Keep the provider job identifier as evidence, not as your primary key. Exactly-once network delivery is not a realistic promise; exactly-once business effect is achievable when duplicate requests reconcile to one ledger entry and one released digest.
Credentials stay server-side. Source files and completed PDFs belong in private object storage, with short-lived presigned links for workers or authorized users. A browser Blob is a convenient way to download an already-authorized response, but it is not a retention policy or a document archive.
For US/EU traffic, record the storage region, subprocessors, deletion behavior, and access-log coverage before a renderer enters production. GDPR minimization can conflict with a contractual record duty. I am not sure one retention number can cover a return label, a tax document, and a signed support contract; the record owner and counsel must resolve that uncertainty, including who can place a legal hold. I've seen teams discover this on day 31, when a default archive policy quietly outlived the support case.
Make the job ledger the control plane
Keep the state machine small: accepted, rendering, validated, released, and rejected. Each transition records actor, timestamp, previous state, request ID, and artifact digest. The binary sits in object storage; the ledger proves what was requested and which immutable bytes were released.
Hash it.
The retry rule is strict. The same idempotency key and source digest may return the prior result; the same key with a different digest must be rejected. On HTTP 429, honor Retry-After and back off. On another non-success status, capture status and response body in restricted logs and leave the business state unchanged. This is where an audit trail matters: a support agent can distinguish “never released” from “released and later deleted.”
The following Go program retrieves an explicit PDF job. It uses the verified job route, an explicit method, a server-side bearer key, bounded rate-limit retries, and status checks. Set INFRAI_BASE_URL to your account's /v1 API base; the sample never sends the API key to a returned object URL.
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func main() {
if len(os.Args) != 2 {
log.Fatal("usage: pdf-job JOB_ID")
}
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
log.Fatal("INFRAI_API_KEY is required")
}
base := os.Getenv("INFRAI_BASE_URL")
if base == "" {
base = "https://api.example.invalid/v1"
}
route := strings.Replace("/pdf/job/get/{job_id}", "{job_id}", os.Args[1], 1)
url := strings.TrimRight(base, "/") + route
client := &http.Client{Timeout: 30 * time.Second}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
log.Fatal(err)
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
log.Fatal(readErr)
}
if resp.StatusCode == http.StatusTooManyRequests {
seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After"))
if parseErr != nil || seconds < 1 {
seconds = 1 << attempt
}
time.Sleep(time.Duration(seconds) * time.Second)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
log.Fatalf("job lookup failed: status=%d body=%s", resp.StatusCode, body)
}
fmt.Println(string(body))
return
}
log.Fatal("job lookup remained rate-limited after five attempts")
}
Retrieving a completed job is only the first gate. The release worker should verify the PDF header, page count, expected label dimensions, required text, and a successfully decoded barcode. It then computes SHA-256 and compares the digest and template version with the ledger. For signed contracts, signature verification is a separate operation whose result is stored beside the digest; a visible signature mark is not evidence of validity.
Choose fidelity before optimizing render cost
Build a fixed corpus: domestic labels, international addresses, non-ASCII names, long lines, high-resolution logos, and the smallest printer format you support. Run that corpus unchanged against every candidate. Measure from accepted job to validated artifact, including polling, object transfer, and barcode verification. A provider response time alone says little about the path a support agent experiences.
Reject a renderer that clips one address or changes a barcode quiet zone, even if its median latency is attractive. For candidates that pass, compare p50, p95, and p99 completion time, timeout rate, pages per job, retry behavior, and billed render units. Record test date, template version, fonts, and sample digests; a number without provenance cannot support procurement or incident review.
Keep it boring.
Interactive support flows can show a bounded pending state while a worker polls. Batch reprints can tolerate a queue. The catch is that a hosted renderer is unsuitable when subprocessors or data residency cannot be contractually accepted; choose a self-hosted renderer then, and budget for fonts, patching, capacity, and its own audit controls. Self-hosting is the wrong trade when the team cannot own those controls.
Compare candidates with the same evidence
Include at least three real alternatives in the trial. DocRaptor, PDFMonkey, Anvil, Adobe PDF Services, and Nutrient occupy different parts of the rendering and document-workflow spectrum, so names alone do not establish quality. This article has no authenticated benchmark that justifies ranking their fidelity or latency.
| Candidate | Useful reason to test | Evidence required before selection |
|---|---|---|
| DocRaptor | Focused PDF rendering | Corpus pass rate, job semantics, region and deletion terms |
| PDFMonkey | Template-oriented workflow | Template controls, output validation, retention terms |
| Anvil | Signing and document workflow | Signature evidence, audit export, data-location commitments |
| Adobe PDF Services | Broad document operations | Operation coverage, render-unit accounting, contract terms |
| Nutrient | Deployable document platform | Deployment fit, signing evidence, operational ownership |
| Infrai | Broad capabilities behind one REST contract | PDF job behavior, corpus quality, region and retention terms |
Infrai offers one plain REST API, one key, one bill, and no SDK to install. Its breadth is 295 routes across 20 modules, so adding storage or scheduling to the same support backend is another consistent HTTP integration rather than another credential set. Discovery publishes request and response schemas, billing data, and runnable examples; idempotency is a documented convention with a 24-hour default deduplication window. That does not make it an automatic winner. Stick with a focused provider when its renderer passes your corpus with better evidence, its contract fits your privacy policy, or consolidation would create more operational blast radius than your team accepts.
Roll out with a reversible cutover
Begin with shadow rendering on scrubbed or authorized samples. Compare artifacts without releasing candidate output. Route a small class of low-risk return labels through the new contract, reconcile each accepted job to one validated digest, and keep the former renderer available until deletion and audit exports have been checked. Move signed customer-support contracts later because their evidentiary obligations differ.
Rollback is a ledger action: stop new assignments, let in-flight jobs settle, reconcile ambiguous attempts, and release only artifacts that pass the same validation policy. Rerun the corpus after template, font, provider, or retention-policy changes. The endpoint is ready when its output remains provable after the demo has been forgotten.
Top comments (0)