Inkspan › Guides › How to generate a PDF from a template in Node.js
Most documents are one layout rendered against changing data: the same invoice for a thousand customers, the same certificate for every course completion. The /v1/generate/template endpoint separates those concerns — you POST a Jinja2 template plus a JSON data object, and Inkspan renders the template and converts the result to PDF in one request.
Keeping the template in the request rather than in your codebase means non-engineers can change document layout without a deploy, and you can store templates in your own database per tenant.
import fs from 'node:fs';
const res = await fetch('https://api.inkspan.dev/v1/generate/template', {
method: 'POST',
headers: {
'X-API-Key': process.env.INKSPAN_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"template": "<h1>Invoice {{ number }}</h1>{% for i in items %}<p>{{ i.desc }}: ${{ i.amt }}</p>{% endfor %}<h3>Total: ${{ total }}</h3>",
"data": {
"number": "INV-1042",
"items": [
{
"desc": "Pro Plan",
"amt": 49
}
],
"total": 49
}
}),
});
if (!res.ok) throw new Error(await res.text());
fs.writeFileSync('document.pdf', Buffer.from(await res.arrayBuffer()));
| Name | Type | Required | Description |
|---|---|---|---|
template | string | yes | A Jinja2 template. Supports {{ variables }}, {% for %} loops, {% if %} conditionals, and filters. |
data | object | yes | JSON object whose top-level keys become template variables. |
filename | string | no | Filename in the response's Content-Disposition. |
Returns a PDF as a binary stream with Content-Type: application/pdf and Content-Disposition: attachment; filename=document.pdf. Write the response body straight to disk — do not parse it as JSON.
{% for i in items %}…{% endfor %}, not {{#each}}.data become variables. Passing {"data": {"invoice": {...}}} means you reference {{ invoice.number }}, not {{ number }}.|e filter if they may contain HTML, or they will be interpreted as markup.| Status | Meaning | Fix |
|---|---|---|
401 | Invalid or missing API key | Send your key in the X-API-Key header. |
402 | Monthly quota exceeded | The response reports used/quota. Upgrade at inkspan.dev/#pricing. |
422 | Missing or invalid parameter | Check required fields in the table above — the body names the offending field. |
Failed requests do not count against your quota; only successful ones are metered.
Templates are sent per request, so store yours in your own database or object storage and send it with each call. This keeps tenant-specific layouts under your control.
Yes — standard filters such as |round, |upper, and |default are available for formatting values inside the template.
Format on your side before sending, or use a filter in the template. Sending pre-formatted strings is the more predictable option for money.
Get a free API key →Related Generate guides: generate a PDF from HTML · convert Markdown to PDF