How to generate an invoice PDF from JSON (with a template)
To generate an invoice PDF from JSON, write an HTML invoice template with {{ placeholders }} for the customer, line items, and totals, then POST that template plus a JSON data object to Docweave's /api/v1/pdf endpoint. Docweave renders the merged HTML in headless Chromium and returns the finished PDF in the response body. Store the template once as a reusable templateId, and always attach an idempotencyKey (your invoice number is ideal) so a network retry never generates — or bills — the same invoice twice.
Most invoicing bugs aren't rendering bugs — they're process bugs. A checkout webhook fires twice, a retry re-sends the same request, and now a customer has two PDFs and, worse, two charges. The fix isn't fancier PDF code; it's separating your invoice **data** (JSON, from your database) from your invoice **layout** (an HTML template), and making the render call idempotent. Here's the whole flow, end to end.
Step 1: Write the invoice as an HTML template
A template is just HTML with placeholders. Docweave merges your JSON into it before rendering, so you write the invoice once — logo, terms, a line-item table — and reuse it for every customer. Keep the CSS inline or in a <style> block; there's no external stylesheet fetch at render time to worry about.
<!doctype html>
<html>
<head>
<style>
body { font-family: -apple-system, sans-serif; color: #111; margin: 40px; }
h1 { font-size: 20px; }
table { width: 100%; border-collapse: collapse; margin-top: 24px; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }
.totals { margin-top: 16px; float: right; text-align: right; }
.totals .grand { font-weight: 700; font-size: 16px; }
</style>
</head>
<body>
<h1>Invoice {{invoice.number}}</h1>
<p>Bill to: {{customer.name}}<br>{{customer.email}}</p>
<p>Issued: {{invoice.issuedAt}} Due: {{invoice.dueAt}}</p>
<table>
<thead>
<tr><th>Description</th><th>Qty</th><th>Unit price</th><th>Line total</th></tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{this.description}}</td>
<td>{{this.qty}}</td>
<td>{{this.unitPrice}}</td>
<td>{{this.lineTotal}}</td>
</tr>
{{/each}}
</tbody>
</table>
<div class="totals">
<p>Subtotal: {{invoice.subtotal}}</p>
<p>Tax: {{invoice.tax}}</p>
<p class="grand">Total: {{invoice.total}}</p>
</div>
</body>
</html>This is the same shape you'd use for a receipt or a quote — swap the fields and the totals block. If you're building the invoicing flow itself rather than just the PDF, the invoices use case page has the broader pattern (numbering, tax fields, storage).
Step 2: POST the template and data together
For a one-off render, send the template HTML inline alongside the data. Docweave merges and renders in one call — no separate upload step needed to get started:
curl https://docweave.dev/api/v1/pdf \
-H "Authorization: Bearer dw_live_your_key" \
-H "Content-Type: application/json" \
-H "Accept: application/pdf" \
-d '{
"source": {
"template": "<!doctype html>... (the HTML above) ...",
"data": {
"invoice": {
"number": "INV-1042",
"issuedAt": "2026-07-14",
"dueAt": "2026-08-13",
"subtotal": "$1,200.00",
"tax": "$96.00",
"total": "$1,296.00"
},
"customer": { "name": "Acme Co", "email": "ap@acme.com" },
"items": [
{ "description": "Consulting — June", "qty": 8, "unitPrice": "$150.00", "lineTotal": "$1,200.00" }
]
}
},
"idempotencyKey": "invoice-1042"
}' \
--output invoice-1042.pdfThe Accept: application/pdf header tells Docweave to return the raw PDF bytes rather than a JSON envelope, and curl --output writes them straight to disk. If the template has a typo or a data field is missing, you get a normal HTTP error back — not a silently broken PDF.
Reuse the template with a templateId
Inlining HTML on every request works for prototyping, but for production invoicing you want the template stored once and referenced by id — smaller requests, and you can update the layout (new logo, new terms) without touching every call site. Create it once via the template endpoint, then render by id:
# One-time: store the template
curl https://docweave.dev/api/v1/templates \
-H "Authorization: Bearer dw_live_your_key" \
-H "Content-Type: application/json" \
-d '{ "name": "invoice-v1", "html": "<!doctype html>..." }'
# -> { "templateId": "tmpl_9f2a..." }
# Every render: just send data
curl https://docweave.dev/api/v1/pdf \
-H "Authorization: Bearer dw_live_your_key" \
-H "Content-Type: application/json" \
-H "Accept: application/pdf" \
-d '{
"source": { "templateId": "tmpl_9f2a...", "data": { "invoice": {...}, "customer": {...}, "items": [...] } },
"idempotencyKey": "invoice-1042"
}' \
--output invoice-1042.pdfStored templates are scoped to your account, so a templateId from one API key can never render under another account by accident.
Don't double-bill: use idempotencyKey
Invoice generation usually sits behind a webhook or a queue, both of which retry. Without protection, a retried request renders the invoice twice — and if that render also triggers a charge or an email, your customer gets billed or notified twice. Set idempotencyKey to something stable and unique per invoice, like the invoice number or order_id + "-invoice". If Docweave sees the same key again, it returns the original result instead of rendering again.
{ "source": { "templateId": "tmpl_9f2a...", "data": { "...": "..." } }, "idempotencyKey": "invoice-1042" }Calling it from Node
In application code you'll typically fetch invoice data from your database, then POST it. See the Node.js guide for a fuller example with error handling; the core of it:
const res = await fetch("https://docweave.dev/api/v1/pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.DOCWEAVE_API_KEY}`,
"Content-Type": "application/json",
Accept: "application/pdf",
},
body: JSON.stringify({
source: { templateId: "tmpl_9f2a...", data: invoiceData },
idempotencyKey: `invoice-${invoiceData.invoice.number}`,
}),
});
if (!res.ok) throw new Error(`PDF render failed: ${res.status}`);
const pdfBuffer = Buffer.from(await res.arrayBuffer());
await fs.promises.writeFile(`invoice-${invoiceData.invoice.number}.pdf`, pdfBuffer);Or from an MCP-connected agent
If an AI agent is generating the invoice — say, from a support or billing assistant — the open-source MCP server exposes the same template + data flow as a tool call, no HTTP boilerplate needed:
await generate_pdf({
source: { templateId: "tmpl_9f2a...", data: invoiceData },
outputPath: "./invoice-1042.pdf",
});That's the whole pattern: one template, JSON per invoice, an idempotency key per attempt. Full field reference and error responses are in the docs, and current per-document rates are on the pricing page.
FAQ
Do I have to store the template, or can I send raw HTML every time?
Either works. Sending inline template HTML on every request is fine for prototyping or low-volume use. For production invoicing, storing it once via the templates endpoint and referencing templateId is cleaner — smaller payloads, and layout changes don't require touching every call site.
What templating syntax does Docweave support in the HTML?
Standard {{ variable }} interpolation and {{#each list}}...{{/each}} loops for line items, as shown above. Anything you can express as HTML/CSS in the template body will render exactly as a browser would render it, since rendering happens in real headless Chromium.
What happens if idempotencyKey is reused with different data?
Docweave returns the result from the original request for that key rather than re-rendering with the new data. Treat the key as tied to one specific invoice attempt — if the invoice content changes, use a new key (e.g., append a version suffix).
Related
Generate your first PDF in minutes.
Get an API key