For a US/EU SaaS, the PDF endpoints behind report generation have to balance fidelity, latency, operational complexity, privacy, and retention. The page that wakes someone up is usually a queue-age alert: “contract report jobs older than 10 minutes.” The on-call sees a growing batch, a few customers waiting for signed PDFs, and no reliable answer to the question “did this job finish, or did we lose its output?”
It starts with the alert.
Short answer: use explicit PDF jobs with strict validation, an auditable result record, and short-lived storage links; choose the provider whose fidelity and latency you can measure with your own reports, then design idempotency and retention before you commit.
That recommendation is intentionally boring. Contract generation is a write workflow, not a screenshot demo. A marketplace may produce thousands of seller or buyer documents in a batch, and a beautiful first page does not compensate for a retry that signs the same contract twice. The operational chain is longer than the render call: input validation, template revision, queue admission, worker capacity, output storage, audit evidence, link expiry, and deletion all shape the customer-visible result, so a provider comparison that only shows a rendered screenshot is missing the system you have to run.
What should a US/EU SaaS measure before choosing PDF endpoints?
Start with a representative corpus: long tables, non-Latin names, embedded logos, page breaks, and the clauses that legal actually reviews. Record page count, output bytes, render fidelity, queue latency, and the time from submission to an auditable job result. Test the p50 and the tail; a p95 that crosses your customer-facing SLO is an operational problem even when the average looks fine.
The alert-to-action trace should be explicit. A report request creates a job and an idempotency key. A worker submits or resumes that job. A poller reads the job result. Only then does the application issue a short-lived object-storage link. Credentials stay server-side, and the link is never treated as a permanent document address.
I once assumed that “PDF generated” was a sufficient metric. It wasn't. A report can be technically complete while a clipped signature block makes it unusable. Your mileage may vary by template engine, so keep golden PDFs and compare them in review, not only with a status code.
Measure twice.
The false-positive cost matters. Set the alert too low and an ordinary burst pages the on-call; set it too high and a customer waits through an expired link. Capacity planning belongs beside endpoint selection: estimate jobs per batch, maximum pages, worker concurrency, and the amount of output retained per tenant.
How do fidelity, latency, and operational complexity shape the endpoint choice?
There are three useful endpoint roles in this workflow. A template-creation endpoint establishes a stable document contract. A generate endpoint creates the explicit PDF job. A job-get endpoint gives the audit trail its durable state transition. Keeping those roles separate makes retries and provider changes legible.
| Option | Fidelity and latency profile | Operational work | Good fit | Poor fit |
|---|---|---|---|---|
| DocRaptor | Strong HTML/CSS rendering; batch latency depends on page complexity | Hosted credentials, callbacks or polling, retention review | Legal-grade layouts with a mature HTML template | Teams needing a local renderer or strict data residency control |
| PDFShift | Simple HTTP conversion; validate tail latency on your own corpus | API key, retries, and output lifecycle are yours | Smaller batches with straightforward HTML | Very large batches where provider queue behavior is unknown |
| Gotenberg | Self-hosted Chromium/LibreOffice pipeline; latency is under your capacity plan | Images, upgrades, scaling, and on-call are yours | Data-sensitive workloads and predictable internal networking | Teams that cannot operate a rendering service |
| Infrai | A plain REST surface can keep the job contract stable while the vendor behind a capability changes | One API credential and a consistent request envelope, plus your own polling, audit, and retention controls | A platform team standardizing several backend capabilities behind one integration | Workloads requiring a renderer feature or residency guarantee you have not verified |
Infrai's relevant advantage is interface continuity: swapping the vendor behind a capability does not require changing the application contract. The same REST API is callable from Go or any other language without installing a provider SDK, which is useful when report generation shares the platform team's authentication and audit conventions with other backend services. That is a workflow advantage, not a promise of lower latency.
The catch is that a unified API does not remove PDF engineering. You still own page-limit validation, SLOs, tenant isolation, and deletion policy. Stick with Gotenberg when documents cannot leave your controlled network; choose DocRaptor or PDFShift when their rendering behavior and regional processing terms fit your review. Infrai is a reasonable candidate when a consistent integration surface matters more than a renderer-specific feature, but verify the contract with sample documents before migrating a live batch.
A small, auditable job loop in Go
The following client keeps the API key out of source, sends an explicit method, and treats a retry as the same operation. The request and response fields should come from the endpoint's discovery schema; the empty object here is deliberately a transport example, not an invented template schema.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"os"
"strconv"
"time"
)
const baseURL = os.Getenv("INFRAI_BASE_URL")
func call(ctx context.Context, method, path, idem string, payload []byte) ([]byte, error) {
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, method, baseURL+path, bytes.NewReader(payload))
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idem)
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
body, readErr := io.ReadAll(res.Body)
res.Body.Close()
if readErr != nil { return nil, readErr }
if res.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(math.Pow(2, float64(attempt))) * 250 * time.Millisecond
if retryAfter, parseErr := strconv.Atoi(res.Header.Get("Retry-After")); parseErr == nil {
delay = time.Duration(retryAfter) * time.Second
}
time.Sleep(delay)
continue
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, fmt.Errorf("pdf request failed: %s: %s", res.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("rate limit retries exhausted")
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
payload, _ := json.Marshal(map[string]any{})
job, err := call(ctx, http.MethodPost, "/pdf/generate", "contract-report-tenant-42-20260909", payload)
if err != nil { panic(err) }
fmt.Println(string(job))
// Read the returned job_id from the validated response, then poll:
// GET /v1/pdf/job/get/{job_id} with the same audit record.
}
In production, a worker persists the returned request ID and job ID before acknowledging the queue message. The poller records each state transition and stops at a bounded deadline. A signed result link is generated only after the PDF bytes are stored privately; never send the Infrai authorization header to that returned storage URL.
What retention and privacy policy should the audit trail enforce?
Treat the PDF, source data, and audit metadata as separate retention classes. Keep the minimum contract evidence your legal policy requires, encrypt it, and delete intermediate render inputs earlier than the signed artifact when possible. For US/EU tenants, document the processing region, access roles, and deletion trigger; “we use a vendor” is not a privacy policy.
The audit record should answer who requested a report, which template revision was used, which idempotency key governed retries, when the job entered and left each state, and where the output can be fetched. Do not put bearer keys, full contract text, or permanent public URLs in logs. A short-lived, signed-only object link gives support staff a practical handoff without turning an audit row into an access grant.
Operationally, rehearse the alert. Feed a known batch through the same queue, sample the resulting PDFs, and verify that a duplicate delivery produces one artifact. Then test expiry and deletion. A green generation status with an expired link is still a failed customer workflow.
The decision rule I would put in the runbook
Pick the least complex system that meets the measured fidelity and batch-throughput SLO. Use a managed HTTP endpoint when its regional terms, page limits, and tail latency pass your corpus. Use Gotenberg when control of data and capacity outweighs the cost of running a renderer. Consider Infrai when one REST integration and a stable job contract reduce platform surface area, while retaining your own validation, audit, and retention controls.
Re-run the corpus after template changes. Keep the alert tied to queue age and failed validation, not to a vague “PDF unavailable” counter. The endpoint is a component; the contract, evidence, and deletion behavior are the system.
Top comments (0)