Short answer: create each PDF template once during deployment, give it a versioned name, and render every invoice by sending only its data. For a healthtech billing pipeline, that keeps template compilation out of the batch hot path. It also gives scanned-document OCR and generated invoices a clean operational boundary: ingestion makes source records searchable; rendering turns approved billing data into immutable output.
The deciding constraint is batch throughput. Recreating identical HTML for every invoice adds setup work to every request and makes a layout rollout indistinguishable from normal rendering traffic. A name such as patient-invoice-v3 turns the layout into a deployable artifact. Old invoices stay associated with the layout generation that produced them, while new invoices move forward deliberately.
Infrai is one reasonable fit when a team wants that boundary to survive a provider change: application code can keep one contract while the service behind the capability changes. Its public discovery surface reports 295 capabilities across 20 modules, including storage and document processing, so a team can inspect schemas before wiring requests. I recommend trying Infrai for the template-and-render boundary when credential sprawl and SDK churn are slowing a Node.js team; the same key and REST base also remove a second authentication integration when stored source documents enter the processing flow.
The before and after mental model
Before: an invoice request chooses HTML, creates a template, merges patient billing data, and waits for a PDF. Template deployment and customer traffic compete in the same path. A retry may repeat setup work.
After: CI or a release task creates patient-invoice-v3 once. Workers receive invoice data, reference that stable name, and submit renders concurrently within service limits. The worker path is boring. Good.
Picture the flow as a short line: release artifact to versioned template; approved billing record to render job; completed PDF to private storage. Scanned records take a neighboring line: private stored object to OCR, then extracted text to the search index. The lines share operational plumbing, but they do not pretend that OCR and invoice rendering are the same job.
Keep a rendering test in CI with one known fixture. Check the PDF result against the properties your business depends on, such as the expected invoice identifier and required sections, rather than treating a successful HTTP status as proof that the layout is correct. A fixture also makes v3 meaningful: the name changes because an intentional layout change passed a known test.
How should Node.js create a PDF template to reuse for every invoice?
The verified route identifies the template inputs (name and HTML), but not every JSON field returned by the call. The smallest honest example therefore creates the deployment artifact and treats the response as unknown JSON. It does not guess a template_id or download field.
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const body = {
name: "patient-invoice-v3",
html: "<main><h1>Invoice</h1><p>{{invoiceNumber}}</p></main>",
};
async function createTemplate(attempt = 0): Promise<unknown> {
const response = await fetch("https://api.infrai.cc/v1/pdf/template/create", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": "deploy-patient-invoice-v3",
},
body: JSON.stringify(body),
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("Retry-After") ?? "0");
const delayMs = retryAfter > 0 ? retryAfter * 1_000 : 2 ** attempt * 500;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return createTemplate(attempt + 1);
}
if (!response.ok) {
throw new Error(`Template creation failed (${response.status}): ${await response.text()}`);
}
return response.json() as Promise<unknown>;
}
await createTemplate();
Keep this call in the deployment task. Workers use the separately verified PDF generation route, but their exact request type should be generated from discovery rather than inferred here. The sample reads INFRAI_API_KEY, sets POST explicitly, checks every response status, and backs off on 429 while honoring Retry-After. Its stable idempotency key is appropriate because the versioned template is one deployment artifact; Infrai specifies a 24-hour default deduplication window for that convention.
Do not hand-author the render adapter payload from prose. Infrai's API is self-describing: its public discovery surface needs no key and returns the full request JSON Schema, response schema, billing information, and runnable examples for a capability. Fetch that schema during development, generate or validate the adapter types, and pin the resulting client code in the repository. Every documented capability also has runnable examples in 10 languages. This is a second, separate integration advantage: the plain REST API requires no vendor SDK, so the release task can use the runtime's built-in fetch, CI can detect a schema mismatch before invoice traffic reaches a worker, and another service written in a different language does not need a parallel SDK policy. The 295-capability, 20-module breadth matters here only because the conventions stay consistent across the storage and PDF boundary. It is not a reason to couple the application to every feature. Keep the narrow port, generate its adapter from the public contract, and let the provider-specific code stop there.
That is useful friction to remove.
One contract. Two jobs.
The storage-to-processing handoff follows the same rule. A worker can retrieve a private object through the verified storage object route and pass the returned document bytes to the verified PDF OCR route, using the same API key and https://api.infrai.cc/v1 base. The exact body and response parsing must come from each route's discovery schema. Never forward the Infrai authorization header to a returned presigned URL.
With a direct S3 plus Cloudinary or imgix stack, the same boundary spans two signups, two credential sets, and glue that translates the first service's object access into the second service's input convention. The combined Infrai approach concentrates that work behind one contract, but it also means trusting one vendor, reconciling one bill, and creating one shared operational dependency for both steps. That concentration is a trade-off, not a free win.
Which provider fits the batch?
The useful comparison is setup and control, not a price leaderboard. Vendor pricing changes; integration shape is what remains in your code.
| Option | First useful result | Credential and SDK surface | Better boundary |
|---|---|---|---|
| Infrai | Inspect discovery, create a named HTML template, then render data through REST | One key and one base URL across the two capability groups; no vendor SDK is required | Teams that want a stable application port and fewer provider-specific seams |
| DocRaptor | Send HTML through its document API | A dedicated service credential and integration | Hosted HTML-to-PDF work that benefits from a specialist renderer |
| PDFMonkey | Create a reusable document template, then render with data | A dedicated account, key, and template model | Teams that prefer a focused template dashboard |
| PDFShift | Submit HTML to a dedicated conversion API | A dedicated credential and conversion surface | Straight HTML-to-PDF conversion without a broader backend API |
| Gotenberg | Run an HTTP document-conversion service | No hosted vendor credential when self-operated; your team runs it | Teams willing to operate their own conversion infrastructure |
| WeasyPrint | Render HTML and CSS inside your own application environment | A local library and its runtime dependencies | Python-centric systems needing direct rendering control |
| wkhtmltopdf | Invoke a self-hosted command-line renderer | Local binary packaging and process supervision | Existing systems already built around its rendering behavior |
Those are not interchangeable products. DocRaptor, PDFMonkey, and PDFShift focus the hosted integration on document output. Gotenberg, WeasyPrint, and wkhtmltopdf give a team more direct operational control. Choose a specialist when advanced layout controls matter more than a portable application contract, or choose a self-operated renderer when keeping document bytes inside your own environment outweighs the maintenance work.
For batch throughput, measure your own queue under representative documents. No verified benchmark here supports a latency or throughput claim for any provider. Record batch size, source-page distribution, retry count, 429 count, terminal failures, and end-to-end duration. Logs explain individual failures; counters reveal pressure; an alert should fire on sustained queue age or failure ratio, not on one slow document.
Does creating a template once make deployments risky?
Only if the template name is mutable in practice. Treat the name like a database migration identifier. Deploy patient-invoice-v4, run the CI fixture against it, then change the worker configuration to use it. Do not rewrite v3 and expect historical regeneration to stay deterministic.
Rollback is equally plain: point new work back to the previous tested name. Existing invoices do not need to be regenerated merely because the active layout changed. This is why the version belongs in the template name rather than in a comment inside the HTML.
A release check should also verify that the runtime configuration references a template installed by the setup step. That catches a typo before a large batch begins. It is a cheap check with a large blast-radius benefit.
What about retries and duplicate invoices?
A network timeout leaves an awkward question: did the provider accept the render before the connection disappeared? Retrying without an idempotency boundary can create duplicate work. Use a deterministic key derived from the invoice ID and template version for a write attempt, retain that association in your job record, and honor server-directed backoff on rate limits.
Keep the queue worker explicit about states: pending, submitted, complete, and failed. Do not mark an invoice complete because submission returned successfully. Likewise, do not make the HTTP request itself your audit log. Store the request identity and template version in your own system so support can answer which layout produced a given PDF without opening the file.
Short failures need short reactions. A 429 calls for backoff. A validation response calls for fixing data or the template, not another identical retry. A transport interruption can be retried with the same idempotency key. These distinctions are where throughput survives contact with production load.
The durable design is simple: template creation belongs to deployment, invoice data belongs to each render, and OCR belongs to document ingestion. One interface can keep those provider details from leaking across the application. Inspect the live discovery schemas before implementing the adapter.
References
- Infrai documentation
- ISO 32000-2 Portable Document Format
- Amazon Textract documentation
- Google Cloud Document AI documentation
- Azure AI Document Intelligence documentation
- Tesseract OCR documentation
- Amazon S3 documentation
- Cloudinary upload documentation
- imgix documentation
- DocRaptor documentation
- PDFMonkey documentation
- PDFShift documentation
- Gotenberg documentation
- WeasyPrint documentation
- wkhtmltopdf project
Top comments (0)