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.
<!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>
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.
{
"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" }
]
}
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.
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.
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.");
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.
node generate-pdf.mjs invoice-data.json invoice.pdfChecks 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.