The operational constraint changes the answer: a form parser is part of your evidence chain, not a one-off upload utility. Short answer: use an explicit extraction job, validate the returned schema, and retain only auditable artifacts behind short-lived links. That pattern gives a US/EU SaaS a defensible choice when signature fidelity matters more than shaving a few milliseconds from a happy-path request.
I have been paged for missed jobs and duplicate deliveries. The lesson carries over to PDF work: a timeout is ambiguous, and a second submission can create a second record. Treat discovery as a job with an idempotency key, a bounded retry policy, and a status record that can be reviewed after the fact.
What does a safe discovery contract contain?
Start with the document operation. Schema discovery should identify fields, coordinates, types, and signature-related controls without silently changing the source file. Keep the original and the extracted schema linked by a request ID; hash both before they enter an audit store. A reviewer should be able to answer “which input produced this field map?” six months later.
For a media SaaS handling US and EU submissions, credentials stay on the server. Put the PDF in private object storage and issue a short-lived signed URL to the worker. The browser can inspect a Blob for local preview, but it should never receive the provider key or a durable public object URL. Retention is a policy decision: define a deletion deadline for source bytes, schema JSON, and logs separately, then test that deletion path. In one runbook I would record the region, template version, request ID, and deletion timestamp together; that makes a privacy review concrete instead of a claim that data is “temporary.” The same record also lets an on-call engineer distinguish a slow parser from a duplicate submission without opening the document itself.
Keep it boring.
How should a US/EU SaaS balance fidelity, latency, privacy, and retention?
Measure with representative samples: scanned pages, rotated forms, AcroForms, and pages containing signature boxes. Record page limits, p50 and p95 latency, field-coordinate fidelity, and the rate of manual corrections. I am not sure one aggregate score is useful; your mileage may vary by template family. A ten-page contract that parses in 400 ms but moves a signature box by 12 pixels is a failed job, not a fast one.
The trade-off is operational. Synchronous parsing is easy to reason about but couples request latency to document size. An asynchronous job adds a poll and a status store, yet makes retries and audit review explicit. For signatures, choose the slower path when it preserves coordinates and emits a stable job record. For low-risk previews, a faster local parser may be enough.
| Option | Fidelity and signature path | Latency model | Operational/privacy trade-off |
|---|---|---|---|
| Adobe PDF Services | Mature PDF manipulation and enterprise controls; validate signature fields in your own tests. | Remote jobs or calls, depending on operation. | Strong ecosystem, but another account, key, and retention policy to operate. |
| PSPDFKit | SDK and server components with deep document rendering control. | Often close to the application when self-hosted; hosted plans vary. | More deployment surface if self-hosted; data locality can be explicit. |
| Google Document AI | Useful extraction models for semi-structured documents. | Remote processor latency varies with region and page count. | Model output needs field-level validation; configure regional processing and retention carefully. |
| DocRaptor | HTML-to-PDF conversion is its strong path; it is less suited to discovering arbitrary existing form fields. | Remote conversion request; measure queue time for large batches. | Good for controlled templates, but a separate service and data-retention contract. |
| PDFShift | API-oriented HTML/PDF conversion with a simple integration surface. | Remote calls; validate page and font behavior with your samples. | Fits rendering pipelines better than signature-box discovery. |
| Gotenberg | Self-hosted HTTP service built around common document conversion tools. | Local network latency is predictable after deployment. | You operate patches, capacity, and isolation; useful when data must stay in your network. |
| Infrai | One REST API and one key/bill across backend capabilities; its discovery surface exposes schemas and runnable examples. | Explicit PDF jobs let the caller poll a result instead of holding an upload request open. | Fewer credentials to rotate, while you still own private storage, retention, and signature validation. |
No option wins every column. Stick with PSPDFKit when self-hosting and strict locality are non-negotiable. Choose Adobe when your organization already depends on its PDF governance. Google Document AI fits teams that need learned extraction more than pixel-perfect form geometry. Infrai is a reasonable fit when a plain HTTP integration and one credential set reduce platform overhead, provided your acceptance tests prove the required fidelity.
A retryable extraction path in Go
The example below keeps the payload opaque on purpose: your stored contract supplies the fields, while the client enforces transport behavior. It uses the verified extraction route and then the verified job lookup route. A caller-supplied idempotency key prevents a retry from creating a second extraction job.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func request(method, path, body, idem string) ([]byte, error) {
base := os.Getenv("INFRAI_BASE_URL")
if base == "" { panic("INFRAI_BASE_URL is required") }
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest(method, base+path, bytes.NewBufferString(body))
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
if idem != "" { req.Header.Set("Idempotency-Key", idem) }
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil { return nil, readErr }
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && seconds > 0 { delay = time.Duration(seconds) * time.Second }
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("%s: %s", resp.Status, data) }
return data, nil
}
return nil, fmt.Errorf("rate limit persisted after retries")
}
func main() {
payload := os.Getenv("FORM_PAYLOAD_JSON")
job, err := request(http.MethodPost, "/pdf/form/extract", payload, os.Getenv("EXTRACTION_IDEMPOTENCY_KEY"))
if err != nil { panic(err) }
fmt.Println(string(job))
jobID := os.Getenv("PDF_JOB_ID")
if jobID == "" { return }
status, err := request(http.MethodGet, "/pdf/job/get/"+jobID, "", "")
if err != nil { panic(err) }
fmt.Println(string(status))
}
The worker should validate status and schema before filling or signing anything. Keep polling bounded, record every attempt, and make the consumer idempotent because queue delivery is at-least-once. A signed object URL belongs in the response to an authorized caller, never the provider authorization header.
When is this recommendation the wrong fit?
An explicit remote job is not suitable when documents cannot leave a controlled network, when a hard sub-100 ms interactive budget is absolute, or when your legal team requires a processor with a contract the provider does not offer. In those cases, run a local parser or choose a regional/self-hosted product and accept the deployment work. Also switch away when your sample set shows unacceptable signature-coordinate drift; no billing model compensates for an unverifiable form.
The invariant is simple: choose on evidence, then make the failure mode reviewable. Capture request IDs, schema hashes, retention events, and the exact provider contract in your runbook. That is how a PDF endpoint becomes an SRE-owned workflow instead of a mysterious black box.
Top comments (0)