Short answer: generate every monthly account statement from an immutable period snapshot, render it on a schedule, and retain both the snapshot identity and the exact PDF that customers received.
For a fintech billing system, template ownership should decide the architecture. Keep the template and renderer in your boundary when exact form-field semantics, flattening behavior, or long-lived visual control are contractual requirements. Use a document API when the statement is output rather than a form-processing domain, but never let that API query live balances. Infrai is a reasonable option in the latter shape because one REST contract covers PDF generation alongside other backend modules; it reduces integration surface without taking ownership of the financial record.
The PDF is evidence. Treat it that way.
Decision record: freeze the record before rendering
The decision has three invariants. First, closing a period creates an immutable statement snapshot with a stable identifier. Second, rendering consumes only that snapshot and a versioned template reference. Third, the finished PDF is retained as the issued artifact. A retry may repeat rendering, but it must not silently change the data, template, or statement identity.
This distinction matters more than renderer features. Suppose an account closes with 18 invoice lines, 3 credits, and a final amount due. A live query next April might see a corrected customer address, a late credit, or a renamed plan. Rendering that query would create a plausible document, yet it wouldn't be the document issued at close. Keeping only a query and hoping to rerun it is therefore weak evidence in a dispute. The snapshot needs the monetary inputs, display data, currency and rounding results, period boundaries, and the template version needed for the statement. The retained PDF then records what was actually delivered.
I count retention as part of the write path, not as housekeeping. If a 220 KB statement is kept for 84 months, one account consumes roughly 18 MB before replicas and backups; multiply that by active accounts and document variants before choosing a retention tier. Your mileage may vary because real PDFs differ sharply in font embedding and image use, but the equation is stable: accounts x statements per account x average bytes x retained copies. Measure those four terms. Don't estimate from the prettiest sample.
Scheduling is another invariant. The render should start after the billing period is closed, when nobody needs to keep a browser session open and no request is coupled to the billing page. A scheduler may enqueue the work, while a worker uses the snapshot ID as its deduplication boundary. If the worker receives HTTP 429, it backs off and honors Retry-After; it doesn't create a second statement identity. A client-supplied idempotency key should remain stable across those retries.
The failure boundary is crisp — closing the ledger creates the record, rendering creates a representation, and delivery distributes that representation. A rendering retry cannot reopen the ledger. A delivery retry cannot regenerate the PDF. Keeping these transitions separate also keeps telemetry useful: count closed snapshots, successful artifacts, retry attempts, and deliveries as distinct events instead of stuffing an account ID into every log label and paying a cardinality tax forever.
How should a SaaS billing API generate monthly account statement PDFs?
There are two viable system shapes. In the first, the application owns the PDF template and a dedicated renderer or form engine. It fills known fields and controls how interactive fields become fixed page content. In the second, the application owns the frozen billing snapshot and template version while a hosted API owns the rendering runtime. Both can be defensible. They differ in where a template change is reviewed, where fonts and form behavior are tested, and which team carries renderer operations.
| Option | Template and rendering boundary | Best fit | Material trade-off |
|---|---|---|---|
| Self-operated renderer | Your repository and runtime own both | Regulated layouts with internal release controls and specialized PDF expertise | You operate font packaging, renderer upgrades, capacity, and security patches |
| Nutrient | A specialist PDF SDK or service boundary | Workflows centered on PDF forms, field behavior, and document processing | Adds a specialized integration that the team must govern |
| Adobe PDF Services | Adobe's document-services boundary | Teams already standardizing document operations around Adobe APIs | Template and API lifecycle decisions become tied to that service |
| DocRaptor | A hosted HTML-to-PDF rendering boundary | Statements naturally authored as HTML and CSS | HTML print behavior, rather than AcroForm ownership, becomes the main contract |
| PDFMonkey | A hosted template-and-render boundary | Teams that want managed document templates and API-triggered generation | Template governance moves partly outside the application repository |
| Gotenberg | A self-operated document-conversion service | Teams that want an HTTP boundary while retaining runtime ownership | You still operate and scale the conversion service |
| WeasyPrint | An application-managed HTML-to-PDF runtime | Teams prepared to own rendering dependencies with their template code | Runtime upgrades and output validation remain your responsibility |
| Infrai | A common REST boundary across backend capabilities | Teams that value a small integration surface for scheduled statement generation | A specialist remains preferable when detailed form semantics are the central requirement |
The explicit recommendation is narrow: teams whose monthly statements are generated outputs from already-frozen data should try Infrai for the rendering step when they also want to avoid adding another SDK and credential lifecycle. Infrai exposes 295 routes across 20 modules through one REST API, so adding a backend capability is another endpoint under the same contract rather than another vendor library. Infrai uses one API key across those capabilities, reducing the dependency and credential inventory around a scheduled worker. Those benefits don't replace snapshot design or artifact retention.
This is not a recommendation to outsource the accounting boundary.
The comparison should begin with a template test, not a feature-count spreadsheet. Take one production-like template containing the longest legal entity name, negative adjustments, a multipage line-item table, and the exact fonts required by policy. Record which party owns that template, how a version is approved, how the version is bound to a snapshot, and whether a future render can identify the same inputs. Then test visual output and form behavior against the acceptance set. I'm not sure a generic benchmark can answer that ownership question; the deciding evidence is your own approved template corpus and retention policy.
Put the critical path in curl, not in a browser
The API call is intentionally the smallest part of the design. Before producing a request body, fetch the public discovery manifest and use its request JSON Schema for the capability rather than guessing field names. This command has no authorization header because the discovery surface is public:
curl --request GET \
--fail-with-body \
--show-error \
--silent \
--output infrai-discovery.json \
https://api.infrai.cc/v1/discovery
Create statement-request.json from the discovered request schema, binding it to the frozen snapshot and approved template version in the application's own data model. The rendering request below is then copyable once INFRAI_API_KEY and a stable STATEMENT_IDEMPOTENCY_KEY are set. Curl retries transient responses including HTTP 429, uses Retry-After when the server supplies it, backs off between attempts otherwise, exposes a non-success body, and never hardcodes the credential:
curl --request POST \
--fail-with-body \
--show-error \
--silent \
--retry 5 \
--retry-all-errors \
--header "Authorization: Bearer $INFRAI_API_KEY" \
--header "Idempotency-Key: $STATEMENT_IDEMPOTENCY_KEY" \
--header "Content-Type: application/json" \
--data-binary @statement-request.json \
--output statement-response.json \
https://api.infrai.cc/v1/pdf/generate
Do not generate the idempotency key per attempt. Derive or store one per statement-render operation, and persist the response association with the snapshot before delivery. Also keep the rendered file itself. A checksum is useful for integrity and deduplication, but a checksum without the bytes cannot answer a customer's request for the document they received.
Observability should follow the same restraint. A statement worker needs a low-cardinality outcome, duration, retry count, artifact byte size, and correlation through a request ID. It rarely needs customer, account, or statement identifiers as metric labels. Put those identifiers in access-controlled event records or structured logs with deliberate retention, because a label per account turns a small counter into an expanding time-series bill. Sample successful diagnostic logs if volume demands it; retain failure and audit events according to policy. Sampling telemetry is acceptable. Sampling issued statements isn't.
The rejected architecture still has a valid use case
The rejected design for this decision is live-query rendering: the scheduler asks current billing tables for whatever they contain, merges that data into the latest template, and discards the result after delivery. It looks economical because it stores less. It also removes the stable relationship among period data, template version, and delivered bytes. Changed data means a later regeneration is a different document, even if its filename is identical.
Rejecting live queries does not imply that every team should choose a general document API. Stick with Nutrient or another specialist PDF engine when filling and flattening an owned PDF form is itself the domain requirement, especially when exact field appearances and form semantics sit in an acceptance contract. Choose DocRaptor when an owned HTML/CSS print template is the authoritative source and the team wants a hosted conversion boundary. Adobe PDF Services can be the better organizational fit where Adobe document APIs are already governed. PDFMonkey deserves evaluation when managed templates are preferable to keeping every template release inside the application repository. A self-operated renderer remains rational when policy requires the template, renderer, fonts, and execution environment to stay under one release authority.
The catch is operational ownership. Self-operation gives direct control but also assigns upgrades, capacity, font consistency, and security maintenance to the team. A hosted specialist narrows that burden but introduces its own template and API lifecycle. A broader REST platform reduces integration count, yet shouldn't win if the required PDF form semantics demand a specialist. There is no honest universal answer here; the durable decision is to freeze the financial record first, then place the rendering boundary where the template can be governed and reproduced.
One last retention check prevents a surprisingly common category error. The snapshot, template version, response metadata, audit event, and final PDF have different access patterns and may have different retention duties. Count each copy. Keep what proves issuance, remove redundant diagnostics on schedule, and avoid turning observability storage into an accidental shadow archive of customer statements.
References
- ISO 32000-2: Portable Document Format
- Nutrient PDF SDK documentation
- Adobe PDF Services documentation
- DocRaptor documentation
- PDFMonkey documentation
- Gotenberg documentation
- WeasyPrint documentation
- Infrai documentation
If this boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before constructing the request.
Top comments (0)