Generate a PDF from HTML and JSON with Node.js

Turn fictional invoice data into a PDF with Node.js. Keep the template in Templatr and send new data from your server for each generation. The response is a binary file you can save or serve from your application, without installing Chromium on your server.

Static demonstration PDF, rendered from the files below with Chromium. All data is fictional.

Requirements

  • Node.js 22 or later. The script uses built-in fetch and modules, with no additional packages.
  • A Templatr account, an API key and available PDF quota.
  • A terminal with curl to upload the template once. The environment commands below use a POSIX shell.

1. Prepare a reusable HTML template

Download this file as invoice-template.html. The template uses nested variables and an items loop. The loop replaces item with each object in the items array; data values are escaped as HTML. Keep styling inside the template: JavaScript is disabled during rendering.

html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Invoice {{invoice.number}}</title>
  <style>
    @page { size: A4; margin: 18mm; }
    body { font: 11pt Arial, sans-serif; color: #172033; }
    h1 { font-size: 26pt; margin-bottom: 8mm; }
    table { width: 100%; border-collapse: collapse; margin: 8mm 0; }
    th, td { padding: 3mm; border-bottom: 1px solid #dbe2ea; text-align: left; }
    th:last-child, td:last-child { text-align: right; }
    thead { display: table-header-group; }
    tr, .total { break-inside: avoid; }
    .total { text-align: right; font-weight: bold; }
  </style>
</head>
<body>
  <h1>Invoice {{invoice.number}}</h1>
  <p>From: {{seller.name}}</p>
  <p>Bill to: {{customer.name}}</p>
  <p>Date: {{invoice.date}}</p>
  <table>
    <thead><tr><th>Description</th><th>Quantity</th><th>Amount</th></tr></thead>
    <tbody>{{ items loop }}<tr><td>{{item.description}}</td><td>{{item.quantity}}</td><td>{{item.amount}}</td></tr>{{ end loop }}</tbody>
  </table>
  <p class="total">Total: {{invoice.total}}</p>
  <p>Sample document with fictional data. No payment is due.</p>
</body>
</html>
Download invoice-template.html

2. Prepare the JSON values

Download invoice-data.json in the same directory. The JSON object is the request body itself, without an extra data wrapper. Format dates, currency and totals in your application. Templatr inserts display values; it does not calculate a total from the rows.

json
{
  "seller": { "name": "Acme Studio" },
  "customer": { "name": "Northstar Co." },
  "invoice": { "number": "DEMO-0042", "date": "2026-09-23", "total": "EUR 299.00" },
  "items": [
    { "description": "Template design", "quantity": 1, "amount": "EUR 199.00" },
    { "description": "Integration review", "quantity": 2, "amount": "EUR 100.00" }
  ]
}
Download invoice-data.json

3. Upload once and save the template ID

Create an API key in Settings, then set TEMPLATR_API_KEY in your server environment. Run this command from the directory containing the template. The JSON response contains template_id. Set TEMPLATR_TEMPLATE_ID to that value and keep it for later generations; you do not need to upload the same HTML for each document.

bash
export TEMPLATR_API_KEY='YOUR_API_KEY'
curl --fail-with-body "https://api.templatr.app/upload" \
  -H "Authorization: Bearer $TEMPLATR_API_KEY" \
  -F "[email protected]" \
  -F "name=Example invoice"
export TEMPLATR_TEMPLATE_ID='TEMPLATE_ID_FROM_RESPONSE'

4. Generate and save the PDF

Save the script below beside invoice-data.json. It sends the JSON to the binary PDF endpoint, checks the HTTP status and content type, and writes invoice.pdf only when the response begins with a PDF header. Keep the API key in server-side environment variables, never in a frontend bundle or public repository.

The .mjs extension enables imports and top-level await without package.json configuration. The script sets a 60-second timeout.

javascript
import { readFile, writeFile } from "node:fs/promises";

const apiKey = process.env.TEMPLATR_API_KEY;
const templateId = process.env.TEMPLATR_TEMPLATE_ID;
if (!apiKey || !templateId) throw new Error("Set TEMPLATR_API_KEY and TEMPLATR_TEMPLATE_ID first.");

const data = JSON.parse(await readFile(process.argv[2] || "invoice-data.json", "utf8"));
const baseUrl = process.env.TEMPLATR_API_BASE_URL || "https://api.templatr.app";
const response = await fetch(`${baseUrl}/pdf/${encodeURIComponent(templateId)}?format=pdf`, {
  method: "POST",
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
  body: JSON.stringify(data),
  signal: AbortSignal.timeout(60_000),
});
if (!response.ok) throw new Error(`PDF request failed (HTTP ${response.status}). Check the API error guide.`);
if (!response.headers.get("content-type")?.includes("application/pdf")) {
  throw new Error("Expected an application/pdf response.");
}
const pdf = Buffer.from(await response.arrayBuffer());
if (pdf.subarray(0, 5).toString() !== "%PDF-") throw new Error("Response does not contain a PDF.");
await writeFile(process.argv[3] || "invoice.pdf", pdf);
console.log("PDF saved.");
Download generate-pdf.mjs

5. Run the script

The first argument is the JSON input and the second is the PDF output. Set the environment variables in the same terminal.

bash
node generate-pdf.mjs invoice-data.json invoice.pdf

Checks and troubleshooting

  • Open invoice.pdf and check DEMO-0042, Northstar Co., both item rows and the EUR 299.00 total.
  • Change the customer name and one description in invoice-data.json, run the script again, and check that the new values appear.
  • Try long names and descriptions before using your production data. Missing variables render as empty strings.
  • For HTTP 401, verify the API key. For 404, verify that the template belongs to that key’s account. For 429, check the account quota and Retry-After header.
  • A timeout does not prove that the server failed to generate a document. Check document history before retrying; repeated successful requests can consume additional quota.
Read API quotas and limits

Continue learning