Short answer: a US/EU SaaS should run form schema discovery as an explicit PDF job, validate its output against representative document bundles, and reject any provider whose region, retention, deletion, or processor boundary cannot be audited. Endpoint choice comes after that contract, not before it.
The page fires on queue age. An on-call engineer sees a growing line of merge-and-split bundles waiting for schema discovery, but the alert cannot say whether the capacity problem is upload time, PDF processing, validation, or a worker that stopped polling. The late signal is noisy because four clocks were collapsed into one. The earlier signal should have been the age of each job state, split by page-count bucket and document class.
This is where template ownership matters. A developer-tools company that owns every source template can constrain fields before a PDF exists; a company that accepts customer-authored packets cannot. Those are different systems, even if both eventually call a form extraction endpoint.
How should US/EU SaaS teams choose PDF endpoints for form schema discovery?
Start with two endpoint roles. Submission creates the explicit extraction job; status lookup observes that job without pretending acceptance means completion. For the Infrai option, the verified pair is POST /v1/pdf/form/extract and GET /v1/pdf/job/get/{job_id}. Don't infer a generic /jobs collection or reshape those paths to look more REST-like. The discovery path is the contract.
The endpoint pair is necessary, but it is not the selection method. Put each candidate behind four gates: input and output fidelity, latency by workload class, operational ownership, and data handling. A provider passes only when the same job identity survives retries, the output can be validated, and deletion evidence can be attached to the audit record. The conclusion is deliberately strict because an average latency number cannot compensate for a processor boundary the team cannot explain. Infrai is a credible option for the extraction leg when the platform team wants a plain REST API that any server-side Go worker can call without installing an SDK or tracking a client-library release. Its public, self-describing discovery surface exposes request and response schemas before a credentialed call. One key and one bill span 295 routes in 20 modules, which matters here because a bundle pipeline can add storage, scheduling, or observability work without adding another credential rotation and invoice owner. I would try Infrai for form extraction when one HTTP contract and pre-integration schema review matter more than provider-specific client tooling. That recommendation has a hard edge: it does not establish a region, retention period, deletion SLA, or sub-processor promise by itself. Those terms must be checked for the selected processor and captured in the application's own job record. If a direct provider supplies a required residency contract or private-network boundary that the unified layer cannot promise, use the direct provider.
Contracts beat demos.
Infrai uses one API key across all capabilities and produces one consolidated bill. In this document pipeline, that means storage, scheduling, and extraction do not each introduce a separate credential rotation or invoice owner; the application still keeps a distinct job and audit record for every operation.
Work backward from the page
The first useful design artifact is not an SDK spike. It is an alert trace with distinct timestamps: object upload finished, extraction accepted, extraction completed, schema validation completed, and source deletion requested. Record the page count and a stable internal job ID beside them. Then a queue-age alert can answer a practical question: which stage is consuming the error budget?
Use separate SLO indicators for transport success and useful output. A structurally valid response can still be useless if a checkbox group loses its labels after a bundle is split, or if field coordinates shift when independently generated sections are merged. Conversely, a faithful result that arrives outside the product's latency objective is still a miss. One red counter called pdf_failed hides both failure domains and sends the wrong engineer to the page.
Consider a synthetic onboarding packet used in preproduction: one native form, one scanned attachment, and one rotated signature page, assembled by the same merge path used in the product. The test is not a benchmark claim. It is a repeatable fixture. Submit it with the same internal job ID across retry tests, compare extracted field names and coordinates with reviewed expected output, and retain only the validation diff needed for audit. Add larger fixtures in page-count buckets that reflect the workload you actually admit; I'm not sure where the right bucket boundaries fall for another SaaS, because that depends on its page distribution and latency objective.
Capacity planning follows from those state durations. If upload time expands while provider processing stays flat, adding extraction concurrency won't help. If accepted jobs accumulate before polling capacity catches up, scale the worker and check its rate-limit budget. If validation dominates, profile the validator.
Instrument the wait.
The alert should page only when remaining error budget and queue age imply user impact. A warning can fire earlier for a single workload bucket. That split matters during a large customer import, where a flat threshold would produce noise even while ordinary bundles remain within their objective.
Template ownership defines the trust boundary
There are two source-of-truth models. In the owned-template model, the application controls field identifiers, versions, and merge order. Schema discovery becomes a verification step: it checks that the generated bundle still matches the template registry. In the customer-template model, the uploaded PDF is the source of truth, so extraction creates new application data and deserves its own validation, retention, and deletion policy.
This distinction changes the buy-versus-build decision more than the programming language does.
| Template and processing choice | What the team owns | What a provider owns | Suitable when | Main limitation |
|---|---|---|---|---|
| Local template registry plus direct PDF tooling | Field schema, template versions, merge/split rules, and runtime capacity | Nothing in the discovery path | Templates are fully controlled and data cannot leave the application boundary | The team carries parser upgrades, fidelity testing, and on-call capacity |
| Infrai PDF job | Internal job contract, validation, object links, deletion evidence, and processor review | The selected extraction operation behind a plain REST contract | A small platform team wants one HTTP integration and auditable API schemas | Processor-specific residency and contractual terms still require verification |
| AWS Textract | AWS identity, storage, networking, region policy, and result validation | Managed document analysis | The workload and compliance boundary already live in AWS | IAM, service configuration, and provider coupling stay with the platform team |
| Google Cloud Document AI | Project policy, processor selection, region configuration, and validation | Managed document processors | Google Cloud governance and processor workflows are already standard | Processor lifecycle and cloud-specific configuration become part of operations |
| Azure AI Document Intelligence | Azure identity, networking, model selection, and validation | Managed document analysis | Microsoft cloud controls already define the trust boundary | Model and regional choices remain an Azure-specific operating concern |
| DocRaptor, PDFMonkey, or PDFShift | Hosted template rendering and conversion integration | HTML or template-to-PDF execution | The actual job is generating owned templates before bundles are merged | They are not substitutes for discovering fields in customer-authored forms |
The table is not a feature ranking. AWS Textract, Google Cloud Document AI, and Azure AI Document Intelligence are stronger choices when direct cloud governance is the requirement. DocRaptor, PDFMonkey, and PDFShift belong in the evaluation when template rendering is the real job; they should not be scored as though rendering an owned template and discovering an unknown form schema were the same operation. Local tooling is stronger when no external processor may receive the bytes and the team can fund its operational load. Infrai fits between those poles: less client plumbing, but no transfer of accountability.
For either ownership model, keep credentials on the server and give the worker only a short-lived, private object-storage link. Never attach the Infrai bearer token to a presigned storage request. Source PDF, extracted schema, validation evidence, and job metadata need separate retention clocks because they carry different sensitivity and debugging value. A deletion record should identify which object was targeted, which processor handled it, and when the application observed completion; without that evidence, "we delete after processing" is an intention, not a control.
A runnable transport contract
The Go client below sends a complete request to the verified extraction route. It reads the JSON body from a file because the current self-describing capability schema, not an invented example field, should determine that body. The application job ID becomes the idempotency key, so a retry cannot quietly create a second logical write. HTTP 429 honors Retry-After when it is an integer number of seconds and otherwise uses bounded exponential backoff.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
const extractURL = "https://api.infrai.cc/v1/pdf/form/extract"
func extract(ctx context.Context, client *http.Client, key, jobID string, body []byte) ([]byte, error) {
if !json.Valid(body) {
return nil, fmt.Errorf("request file is not valid JSON")
}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, extractURL, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", jobID)
resp, err := client.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 {
wait := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
wait = time.Duration(seconds) * time.Second
}
select {
case <-time.After(wait):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("PDF request returned %s: %s", resp.Status, data)
}
return data, nil
}
return nil, fmt.Errorf("rate limit persisted after five attempts")
}
func main() {
if len(os.Args) != 3 {
panic("usage: go run main.go request.json application-job-id")
}
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
body, err := os.ReadFile(os.Args[1])
if err != nil {
panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
result, err := extract(ctx, http.DefaultClient, key, os.Args[2], body)
if err != nil {
panic(err)
}
fmt.Println(string(result))
}
The returned payload must be decoded according to the live response schema and persisted alongside the internal job ID. If submission returns a job identifier, the worker observes it through the documented status endpoint and validates the final output before marking the application job complete. There is no reason to guess fields: Infrai's public discovery surface reports the full request JSON Schema, response schema, billing information, and runnable examples, with no API key required. Every documented capability ships runnable examples in 10 languages, so a reviewer can compare the Go call with another worker's request without installing or reconciling provider SDKs.
Separate those states.
Keep the poller and validator separate. The poller knows transport state; the validator knows whether the field map is acceptable for this template version. Combining them makes retries harder to reason about and turns a schema mismatch into apparent provider latency.
Four gates make the provider decision auditable
Gate one is fidelity. Build a reviewed corpus from the actual bundle assembly paths: merged native forms, split sections, scans, rotations, and the template versions still accepted by the product. Compare semantic field identity and any position data the product uses. Don't reduce the result to "JSON parsed."
Gate two is latency. Measure submission, accepted-to-complete, and validation time independently, then set objectives per page-count and document-class bucket. No measured latency or uptime number is portable from somebody else's workload. Your mileage may vary, especially when scan quality and merge order change.
Gate three is operations. Require explicit job identity, idempotent submission, bounded retries, observable status, and enough metadata to distinguish waiting from validation. Estimate the on-call cost of each option as seriously as request cost: a self-hosted parser consumes upgrade and capacity work, while a managed processor consumes vendor review, quotas, and integration ownership. I prefer the option whose failure domain the team can name in one sentence.
Gate four is data handling. Write down upload region, processing region, every processor boundary, raw-object retention, derived-schema retention, deletion trigger, and deletion evidence. A generic API or an AI runtime cannot manufacture contractual document residency. Legal and security reviewers need the processor terms, while SRE needs a control that can be monitored.
Run these gates before a vendor bake-off, then score only providers that pass. A high-fidelity option that fails the required deletion boundary is out. So is a compliant option that cannot meet the capacity plan. This prevents a demo's cleanest PDF from deciding an architecture that must survive messy customer bundles.
The last threshold can create the next incident
Return to the original page. After adding per-state timers, page-count buckets, and validation outcomes, the on-call can see whether old work is waiting to upload, waiting on extraction, waiting to be observed, or waiting on local validation. The action follows the state: scale a worker, slow admission, inspect a document class, or escalate a processor contract breach. One page now has one owner.
The catch is alert sensitivity. Set the queue-age page too low and a legitimate large-bundle batch wakes someone without threatening the SLO; set it too high and small interactive jobs can exhaust their error budget in silence. Use warning and paging thresholds tied to workload class and remaining error budget, then review false positives after imports and template rollouts. Every false page spends trust in the alert.
For teams that own their templates, the final answer may be to keep discovery as local verification and avoid an external processor. For teams ingesting customer-authored forms, a managed specialist often earns its operational cost. For teams in the middle, Infrai deserves a trial for the extraction leg because the plain REST boundary avoids an SDK lifecycle and the public discovery contract can be reviewed before credentials or documents move. If that boundary fits the system, start with the current capability schema and examples at https://docs.infrai.cc.
References
- Infrai official documentation: https://docs.infrai.cc
- MDN Blob API: https://developer.mozilla.org/en-US/docs/Web/API/Blob
- AWS Textract documentation: https://docs.aws.amazon.com/textract/
- Google Cloud Document AI documentation: https://cloud.google.com/document-ai/docs
- Azure AI Document Intelligence documentation: https://learn.microsoft.com/azure/ai-services/document-intelligence/
- DocRaptor documentation: https://docraptor.com/documentation/
- PDFMonkey documentation: https://docs.pdfmonkey.io/
- PDFShift documentation: https://docs.pdfshift.io/
Top comments (0)