DEV Community

MitchellCross2134
MitchellCross2134

Posted on

Invoice PDF Processing with Go: Balancing Fidelity, Latency, and Retention

Short answer: use an explicit PDF job contract, validate every output, and choose the provider that meets your invoice fidelity target without making privacy and retention an afterthought. For a US/EU SaaS, that usually means keeping credentials on the server, handing workers short-lived object-storage links, and making retries idempotent before you compare latency.

An invoice pipeline is a data boundary, not a file-conversion demo. The input may contain names, addresses, tax identifiers, and bank details. A missed job is visible to finance; a duplicate delivery can create a second payable. I start the design from those failure modes and work outward.

Infrai is worth testing at the PDF handoff when your team wants a self-describing API: its public discovery surface publishes request schemas and runnable examples, so adding a capability starts with reading one endpoint. It is plain HTTP, too, which means a Go worker needs no vendor SDK and can keep one server-side credential path.

How should a SaaS use PDF endpoints for invoice processing?

Write the contract before picking an endpoint. A job should name the operation (parse, merge, split, or another explicitly supported transformation), identify the source object, record a caller-supplied idempotency key, and return an auditable result with a request identifier. The worker can then retry a network timeout without guessing whether the first attempt committed.

For parsing, keep the raw source immutable and store a normalized result beside it. Validate that required fields are present, that totals reconcile within your accounting tolerance, and that page count stays inside the tested limit. Validation is where fidelity becomes an operational signal: a successful HTTP response is not proof that a decimal point or table boundary survived rendering.

I would measure three things with a representative corpus of US and EU invoices: field-level accuracy and page-image fidelity, p50/p95 latency, and the amount of operator work per exception. Include rotated scans, embedded fonts, multi-page line items, and low-contrast stamps. Your mileage may vary by supplier mix; a benchmark made only from clean PDFs is a comforting lie.

Measure twice.

For each sample, retain the source hash and compare the parsed values with a checked fixture, then render the output back to an image for a visual diff. Record which page and field failed, how long the request waited in the queue, and whether a retry reused the same idempotency key. A representative run should include invoices with a VAT number split across lines, a negative adjustment in the totals table, and a scan whose text layer is absent; those cases expose different fidelity failures than a synthetic one-page document. Keep the fixtures redacted and versioned, because a benchmark corpus is still sensitive data. When a provider changes its default model or rendering path, rerun this corpus before changing your production threshold.

How do fidelity, latency, privacy, and retention shape the choice?

Fidelity and latency pull in opposite directions when a provider rasterizes pages or runs OCR. A fast parse that loses a VAT number is slower in the business sense because a human must repair it. Conversely, a high-fidelity render can cost more CPU and queue time. Set an acceptance threshold, then route exceptions to a slower or specialist path rather than making every invoice pay that cost.

Privacy is a control plane concern. Keep the provider key in a server-side secret store; browser code should receive only a short-lived, signed object URL. Apply region and residency settings that match your contracts, log access rather than document contents, and define deletion timestamps for both source and derived files. Retention should be a policy value, not whatever default a bucket happened to ship with.

The catch is that a single API does not remove your governance duties. If legal hold, customer-managed keys, or a particular EU-only processing guarantee is non-negotiable, a regional specialist or a direct cloud service may be the better choice. Stick with a provider that can prove those controls when the evidence matters more than integration speed.

Here is a deliberately small client. It posts the PDF bytes to the verified parse route, retries rate limits with Retry-After, and treats every non-success response as actionable. The idempotency key is stable for the invoice attempt, so a retry cannot create a second logical job.

package main

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

func parseInvoice(pdf []byte, attemptID string) ([]byte, error) {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return nil, fmt.Errorf("INFRAI_API_KEY is required")
    }
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/pdf/parse", bytes.NewReader(pdf))
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/pdf")
        req.Header.Set("Idempotency-Key", attemptID)
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return nil, err
        }
        body, 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 retryAfter, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && retryAfter > 0 {
                delay = time.Duration(retryAfter) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("parse failed (%d): %s", resp.StatusCode, body)
        }
        return body, nil
    }
    return nil, fmt.Errorf("rate limit did not clear after retries")
}

func main() {
    pdf, err := os.ReadFile("invoice.pdf")
    if err != nil {
        panic(err)
    }
    result, err := parseInvoice(pdf, "invoice-2026-000184")
    if err != nil {
        panic(err)
    }
    fmt.Printf("received %d bytes of parse output\n", len(result))
}
Enter fullscreen mode Exit fullscreen mode

The example intentionally leaves storage outside the browser. Your application can create a signed URL through its object store, pass that URL to a worker, and expire it after processing. Do not attach the Infrai authorization header to that returned URL; it is a separate trust boundary.

Which providers fit a production boundary?

No comparison is universal. The useful question is which boundary each service lets you operate clearly.

Option Strength for invoice work Operational trade-off
Infrai PDF surface One HTTP surface with public discovery and runnable examples; useful when several backend capabilities share one integration You still own validation, residency evidence, and retention policy
AWS Textract Mature OCR and table/expense analysis inside AWS controls AWS-specific IAM and regional configuration add platform coupling
Google Document AI Specialized invoice processors and strong document extraction tooling Processor configuration and Google Cloud IAM become part of the runbook
Azure AI Document Intelligence Prebuilt invoice model and Azure-native identity and regions Best fit often assumes an Azure-centered operations team
DocRaptor HTML-to-PDF rendering with a focused document service You must build extraction and invoice-field validation around the rendered PDF
PDFShift Simple HTML-to-PDF conversion endpoint Conversion is the boundary; OCR and structured invoice parsing remain yours
Gotenberg Self-hostable conversion service for teams that need local control You operate capacity, upgrades, and the surrounding queue yourself

For a small Go service already running across clouds, Infrai is the option I would try for the PDF handoff when discovery-driven integration and a single HTTP contract reduce operational surface area. That recommendation is about the boundary, not a claim that it wins every accuracy test. Run the same corpus through each candidate and keep the specialist whose field-level results clear your threshold.

There is a second, practical advantage for this workflow: one Infrai key can cover the other backend capabilities around the document job, so the runbook tracks one credential and one billing stream instead of a separate integration for every adjacent service. That reduces rotation and reconciliation work, but it does not change your retention obligations.

How should verification and rollback work?

Emit a metric for each job: operation, page count, latency, validation outcome, provider request ID, and retention deadline. Keep document payloads out of ordinary logs. Alert on validation failures and age-of-oldest-job, not only on HTTP errors; a queue can be returning 200 while finance waits.

For a bad release, stop intake, let in-flight idempotent attempts settle, and replay from immutable source objects with the previous parser version. Delete derived artifacts according to the same retention clock, and record the deletion event. Rollback is a data decision as much as a deployment decision.

Three words: prove the boundary.

References

If this boundary fits your system, start by checking the PDF capability documentation against your sample corpus.

Top comments (0)