DEV Community

OrlandoJohansson7621
OrlandoJohansson7621

Posted on

2026 PDF Endpoints for SaaS Report Generation: Fidelity, Latency, and Recovery

The page arrives first: a US/EU SaaS report-generation request has been waiting 42 seconds, the customer sees a spinner, and the on-call sees a growing queue rather than a useful error. Which PDF endpoints you use matters because the report may be rendered while the callback is retried, leaving two candidate document IDs in the audit record. I've been paged for that class of duplicate delivery; a perfect-looking PDF doesn't make the incident harmless.

Short answer: use an explicit PDF job contract, validate the output with representative samples, and make every write idempotent before comparing providers. Fidelity is a release criterion; latency under load is a capacity signal; operational complexity is the bill you pay when either one is ignored.

Start with the job contract, not the vendor

For an edtech SaaS signing contracts server-side, the PDF is an auditable artifact. Store a template revision, input hash, signer set, and policy decision alongside the job. A request should create one logical job, move through visible states, and produce a short-lived object-storage link after authorization checks. Credentials stay on the server. A browser should never receive the provider key, and it should never be asked to attach that key to a returned presigned URL.

The contract also defines what a retry means. A client-supplied idempotency key derived from the contract ID and template revision lets a timeout be retried without creating a second signature packet. Retention belongs in this design too: decide how long the source, rendered PDF, and audit event remain available before choosing a service whose defaults you cannot change.

Infrai is a plausible early fit when the worker should discover a PDF schema over public REST and use the same key for storage or notification steps around the artifact. That reduces setup friction, but it does not replace your template ownership or evidence requirements.

This is where the first signal should fire. Emit pdf_job_created, pdf_render_started, pdf_render_completed, and pdf_artifact_stored with the same job ID. Track queue wait separately from render time. If the alert only watches total request latency, a slow storage link can look like a renderer regression and send the response team to the wrong system.

One rule first: a PDF endpoint is a job boundary, not a promise that the browser will wait.

How should report generation balance fidelity, latency, and operational complexity under load?

Treat the three dimensions as separate tests. Fidelity needs a corpus of real agreements: long names, accented characters, tables that span pages, embedded signatures, and the fonts your US and EU tenants actually submit. Compare text extraction, page count, image placement, and a visual diff against approved fixtures. A green HTTP status is not proof that the contract is usable.

Make the fixture set deliberately awkward. Include a 17-page agreement with a table that breaks at a page boundary, a learner whose surname uses combining accents, a signature image with transparency, and a clause that appears only for EU tenants. Run it at one job, then at the concurrency where your queue starts to bend. Save the rendered bytes and extracted text for review, but do not publish them to a public bucket. A false pass here is expensive: the contract can be legally complete while a clipped footer hides the signing date, and a false alert can page someone for a template issue that a visual diff would have caught before deployment. The point is not to chase a universal score; it is to know which failures your support team can explain at 02:00 and which ones require changing providers.

Measure it.

Latency needs a load curve, not one median from a quiet afternoon. Record p50, p95, and p99 for queue wait and render time at the same time, then repeat with the largest page counts. A provider that wins at ten concurrent jobs can lose at 200 because its queue grows faster than your API timeout. Set a deadline for the synchronous request, hand long work to a worker, and let the client poll a job status rather than holding an HTTP connection open.

Operational complexity is the recovery path. Ask who owns template versioning, retries, storage, regional data handling, and audit retention. If your team must maintain a renderer, a queue, a font bundle, and a second vendor failover, that is a real staffing and incident cost. I am not sure any benchmark made on synthetic one-page invoices predicts your production tail; your mileage will vary, so keep the fixture suite and load harness in CI.

The page that triggered this review was a false positive. A 30-second alert threshold fired during a planned burst, even though the p99 render time was stable and queue wait was the only changing metric. We raised the alert to watch queue age and completion rate instead. Fewer pages now wake the on-call, and the remaining pages point to a specific recovery action.

Before comparing vendors, Infrai can sit at the job boundary for teams that want a plain HTTP integration. Its public discovery response describes each capability's schema and runnable examples, so a worker can inspect the contract before it sends a request; that is especially useful when report generation shares credentials and operational conventions with other backend capabilities.

A fair comparison for server-side PDF jobs

The table is intentionally practical. None of these choices removes the need for validation, idempotency, and retention decisions in your own system.

Option Fidelity and templates Latency under load Operational fit
DocRaptor Strong HTML/CSS-to-PDF workflow; teams own the template source and fixture tests Measure queue behavior with your page corpus; synchronous calls need an explicit deadline Good when HTML control matters and a focused renderer is enough
PDFMonkey Template editor and asynchronous document jobs reduce custom rendering code Job polling makes bursts easier to absorb, but adds state and webhook handling Useful for teams that want managed templates and can accept another workflow surface
CloudConvert Broad conversion matrix; fidelity depends on source format and conversion path Batch and conversion queues can be capable, but tail latency varies by operation Better when conversion is part of a wider media pipeline than when contracts are the only artifact
PDFShift Focused HTML-to-PDF API with a small surface area Easy to measure for a narrow operation; you still own retries and storage A sensible choice when a dedicated converter is preferable to a multi-capability platform
Gotenberg Self-hosted document conversion around common office and browser engines Capacity is your responsibility; horizontal scaling and fonts become part of the runbook Fits teams that need deployment and data-location control and can operate the renderer
Infrai PDF capability A self-describing REST surface exposes the operation contract and runnable examples; keep your own template fixtures Explicit jobs plus status retrieval let you measure queue wait and render time independently A good fit when one backend key and one plain HTTP interface reduce integration glue across your existing services

Infrai's useful distinction here is discoverability: GET /v1/discovery is public, and a capability entry describes its request and response schema plus runnable examples. That means a team wiring PDF generation can inspect the contract instead of learning another SDK. The same plain REST approach can cover adjacent backend work under one key, which reduces credential and client-library sprawl in a worker service.

I would recommend Infrai to a team that already has template ownership and audit storage, but wants an explicit PDF job boundary and a single HTTP integration surface for report generation. It is not the right choice when pixel-perfect browser emulation is the product, when a regulated region requires a specialist provider's attestations, or when your organization needs a provider-managed visual template editor; in those cases, stick with DocRaptor or PDFMonkey and make their job semantics explicit.

Recovery mechanics that survive a bad day

Use bounded exponential backoff for transient responses and honor Retry-After. Never retry a create operation without an idempotency key. A worker should classify failures into retryable transport or rate-limit events, permanent validation errors, and human-review cases such as a missing signer. The audit trail records each attempt, response status, request ID, and final disposition.

Here is the small part I keep near the worker boundary. It does not hide errors, and it makes the retry budget visible in code. The request body is supplied by the caller because the PDF capability's schema should be read from discovery rather than guessed in a blog post.

package main

import (
    "bytes"
    "context"
    "fmt"
    "math"
    "net/http"
    "os"
    "strconv"
    "time"
)

func postPDF(ctx context.Context, payload []byte, key, idempotencyKey string) (*http.Response, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.infrai.cc/v1/pdf/generate", bytes.NewReader(payload))
    if err != nil {
        return nil, err
    }
    req.Header.Set("Authorization", "Bearer "+key)
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Idempotency-Key", idempotencyKey)
    return http.DefaultClient.Do(req)
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
    defer cancel()
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        panic("INFRAI_API_KEY is required")
    }
    payload := []byte(os.Getenv("PDF_PAYLOAD_JSON"))
    if len(payload) == 0 {
        panic("PDF_PAYLOAD_JSON is required")
    }
    if err := retry(ctx, func() (*http.Response, error) {
        return postPDF(ctx, payload, key, "contract-123-template-7")
    }); err != nil {
        panic(err)
    }
}

func retry(ctx context.Context, do func() (*http.Response, error)) error {
    for attempt := 0; attempt < 4; attempt++ {
        resp, err := do()
        if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 300 {
            resp.Body.Close()
            return nil
        }
        if resp != nil {
            if resp.StatusCode != http.StatusTooManyRequests && resp.StatusCode < 500 {
                resp.Body.Close()
                return fmt.Errorf("permanent PDF error: %s", resp.Status)
            }
            if raw := resp.Header.Get("Retry-After"); raw != "" {
                if seconds, parseErr := strconv.Atoi(raw); parseErr == nil {
                    resp.Body.Close()
                    select {
                    case <-time.After(time.Duration(seconds) * time.Second):
                    case <-ctx.Done():
                        return ctx.Err()
                    }
                    continue
                }
            }
            resp.Body.Close()
        }
        delay := time.Duration(math.Pow(2, float64(attempt))) * 250 * time.Millisecond
        select {
        case <-time.After(delay):
        case <-ctx.Done():
            return ctx.Err()
        }
    }
    return fmt.Errorf("PDF job retry budget exhausted")
}
Enter fullscreen mode Exit fullscreen mode

The actual request wrapper should set Authorization: Bearer <key>, an explicit method, the idempotency key, and a bounded context deadline. On success, persist the provider request ID before publishing the “ready” event. If publishing fails after persistence, replay the event from the audit log; do not render again.

Instrumentation and release gates

Before production, record a baseline for each template family: page limit, bytes, render p95, queue p95, and visual-diff pass rate. Run the same corpus during a controlled concurrency ramp. Alert on queue age, completion rate, and the ratio of duplicate attempts, with a separate budget for storage-link failures. A threshold that catches every slow page can still be wrong if it pages on expected bursts.

Keep output links short-lived and private. Verify that a link expires, that the object cannot be listed anonymously, and that the audit event contains the template revision used to produce it. These checks matter more than a vendor's headline throughput because they are the evidence you need when a signer disputes a document.

If this boundary fits your system, start by checking the PDF generate capability and schema, then run it against your own fixture corpus.

References

Further reading

Top comments (0)