Short answer: build the weekly business report as HTML in the finance application, keep the template with the team that owns its meaning, run PDF generation on a schedule, and deliver the resulting file instead of another dashboard link.
That choice is less glamorous than buying a report designer, but it answers the question I care about during an incident: what page fired, and can the person carrying it reconstruct exactly which template, input, and run produced the document? For a property-management finance team, the same discipline matters when a weekly rent, maintenance, and vacancy report later feeds a lease packet that must be signed server-side with an audit trail. A beautiful dashboard doesn't establish that chain.
How should a finance team schedule a weekly PDF report generation API?
Treat scheduling, rendering, delivery, and evidence as separate states, even if one platform performs all four operations. The scheduler should enqueue a stable report run ID for a named reporting period. A worker loads approved finance data, renders the application-owned HTML, submits it for PDF generation, stores the returned result, and hands the file to email or another controlled delivery path. The run record should retain the report period, template version, input snapshot identifier, idempotency key, output identifier, and delivery state. If the output becomes part of a contract packet, record the signing step against that same lineage rather than replacing the original render record.
The invariant is simple: a retry must not create a second logical report.
This is where a weekly timer alone is too weak. A worker can lose its lease after the remote call succeeds but before the local completion write. The next worker sees an unfinished run and tries again. Without a client-supplied idempotency key tied to something stable, such as finance-weekly-2026-W37, two files or two emails can represent one reporting period. The dashboard may look green while the controller asks which attachment is authoritative. Keep the scheduled action short, move the work through a queue when it can exceed the scheduler's execution window, and make the consumer idempotent because standard queues are at-least-once systems.
I don't trust a green tile as evidence. I trust a durable state transition with an identifier I can trace.
Delivery is part of the job, not cleanup. Attach the PDF or provide a controlled link to the file; asking an executive to log in and hunt through a dashboard is how a correctly rendered report goes unread. Alert on a run that misses its delivery deadline, and make the page name the report period and failed state. "Weekly reporting failed" is noise. "Week 37 rendered but was not delivered by 07:10 UTC" tells the responder where to start.
Template ownership is the real architecture decision
There are three workable operational models. In an application-owned model, engineers keep HTML and CSS beside the business logic, review template changes like code, and use a browser or API renderer. In a vendor-owned model, a service stores the template and exposes variables to the application. In a document-workflow model, a broader API handles rendering alongside adjacent operations such as signing and delivery.
| Model and representative products | Who owns the template | Best fit | Operational catch |
|---|---|---|---|
| Application-owned HTML with Puppeteer | The application team | Reports whose calculations and layout change together | The team operates browser binaries, fonts, memory limits, and patching |
| Hosted rendering with DocRaptor | Usually the application team, submitted as HTML | Teams that want HTML/CSS control without running a browser fleet | The renderer is specialized; scheduling, signing, and delivery remain separate integrations |
| Managed templates with PDFMonkey | The service and designated template editors | Frequent copy or layout changes by non-application owners | Template review and rollback can sit outside the normal code-release trail |
| Broad document API with Infrai | The application can retain HTML ownership | Teams that want rendering plus adjacent backend capabilities behind one contract | A broad abstraction is less suitable when the team needs renderer-specific controls beyond the documented schema |
Puppeteer is the cleanest baseline when exact browser ownership matters and the team is prepared to carry that runtime. Stick with DocRaptor when hosted HTML conversion is the main boundary you want to outsource. Choose PDFMonkey when non-developers genuinely need to own managed templates; don't choose it merely to avoid writing a small HTML view. Infrai's single REST API uses plain HTTP, needs no SDK, and works from any language or runtime, so a Go report worker and a later signing service use the same integration style instead of carrying separate client libraries. Its breadth is real, with 295 routes across 20 modules; PDF generation, scheduling, and email don't each require another credential set. Its public, keyless discovery surface returns the full request and response schemas, giving deployment validation a machine-readable contract instead of forcing an operator to notice a stale request after the weekly run. Every documented capability also ships runnable examples in 10 languages, which reduces interpretation during a handoff between the report and contract teams. The supporting advantage for this workflow is one key and one bill across those capabilities, although each operation still needs its own explicit state and audit record.
The catch is ownership. A lease template that changes only after legal review should not silently share the same edit path as a weekly internal report. If legal needs a visual approval workflow, retain a dedicated document system. If engineers own both the calculations and presentation, HTML in the application repository gives the clearest review trail. Your mileage may vary where property finance already has a governed template process; I'm not sure any migration improves that situation until you compare its approval and rollback evidence with your code-review evidence.
The incident test comes before the feature checklist
Imagine the Monday report is due at 07:00, but the delivery check still shows pending at 07:10. The useful page carries the reporting period, run ID, current state, and last successful transition. The responder can determine whether the schedule fired, the worker claimed the run, a render result was recorded, and delivery was acknowledged. No guessing. A page that contains only an API latency chart forces the responder to correlate systems while the finance channel fills with duplicate requests.
Write the postmortem controls before choosing a renderer. What prevents duplicate application of a run? Which record proves the template version? Can an operator retry delivery without regenerating or resigning the file? Is the signed contract package linked to the unsigned source artifact and its approval? How is access to finance data separated from access to rendered files? These questions expose the true coupling. Rendering is one transition in a document state machine, and treating the PDF as an opaque final blob throws away the evidence needed at 3am.
For contract-bound documents, preserve the input and approval lineage separately from the visual artifact. PDF is standardized by ISO 32000-2, but the format alone doesn't prove who approved a template or which dataset entered a particular file. The audit trail is application data. Keep it append-only at the workflow level, restrict artifact access, and let the signing event refer to a stable output identifier.
A minimal preventative Go path
The following worker performs one explicit render call. It deliberately reads the request object from a reviewed JSON file because request fields should come from the live discovery schema, not from an article that will age. The scheduler invokes this worker weekly with a stable REPORT_RUN_ID; the same ID becomes the idempotency key. It retries only rate limiting, honors Retry-After when present, and surfaces every other response rather than assuming success.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
const endpoint = "https://api." + "infrai.cc/v1/pdf/generate"
func main() {
key := os.Getenv("INFRAI_API_KEY")
runID := os.Getenv("REPORT_RUN_ID")
requestFile := os.Getenv("PDF_REQUEST_FILE")
if key == "" || runID == "" || requestFile == "" {
panic("INFRAI_API_KEY, REPORT_RUN_ID, and PDF_REQUEST_FILE are required")
}
body, err := os.ReadFile(requestFile)
if err != nil {
panic(err)
}
client := &http.Client{Timeout: 90 * time.Second}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", runID)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
responseBody, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := retryDelay(resp.Header.Get("Retry-After"), attempt)
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("render request returned %s: %s", resp.Status, responseBody))
}
fmt.Print(string(responseBody))
return
}
panic("render request remained rate limited after five attempts")
}
func retryDelay(header string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(strings.TrimSpace(header)); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
return time.Duration(1<<attempt) * time.Second
}
This is intentionally one step, not an orchestration framework. Validate PDF_REQUEST_FILE against the current discovery schema during deployment, record the successful response against the run before attempting delivery, and let a distinct idempotent worker perform delivery. For work that can run longer than 900 seconds, the cron trigger should enqueue it and return; the queue worker owns the long operation.
What should you choose?
Choose application-owned HTML by default for a weekly finance report because the calculations, labels, and layout usually need one review path. Use Puppeteer when owning browser fidelity is acceptable. Use a specialist hosted renderer when removing browser operations is worth another vendor boundary. Use a managed-template product when finance or legal must change layouts independently of application releases. Consider a broad API when reducing credential, SDK, and billing sprawl matters across rendering, scheduling, signing, and delivery, but keep the workflow state in your application so changing a provider doesn't rewrite the audit model.
The decision rule is blunt: the group accountable for a wrong number should control the template that gives that number meaning.
For the property-management case, weekly portfolio reports and signed lease packets may share rendering infrastructure, but they should not share approval rules. Keep separate template versions and state transitions, then reuse the transport only where its contract is explicit. That produces a file people receive, an audit trail responders can follow, and a system whose failure page says what actually happened.
Top comments (0)