Skip to main content

Invoices and receipts from JSON

This guide builds an invoice template in the dashboard and generates PDFs from your code.

1. Start from the library​

  1. In the dashboard, open Templates → Template library.
  2. Pick Invoice (A4) or Invoice (US Letter) and click Use this template.

The builder opens with a live preview on the right:

  • Blocks (left): add headings, text, details lists, addresses, item tables, totals, images, barcodes, signatures, columns and a footer. Drag blocks, or use the arrows, to reorder them.
  • Page & style (left): paper size, orientation, margins, font, colours, language, currency and date format.
  • Properties (right): edit the selected block. Any text box has an Insert field button ({ }) that places a value from your JSON, such as {{customer.name}}.

2. Decide what your JSON contains​

Click Fields & test data.

  • Fields lists every value the template expects. The JSON key is the property name you send. Mark a field Required to have the API reject requests that leave it out.
  • Test data fills in example values for the preview. The values are saved with the template and used as the example in Use via API.

A list field, such as items, becomes table rows. Its columns are defined under the field. A totals block adds up quantity × unit_price, applies discount and tax, and formats everything in the template currency.

3. Save and try it​

Click Save, then Generate PDF to fill the form and download a PDF. This is charged as one PDF.

4. Call the API​

Click Use via API and copy the request. It already contains your template ID and every field.

const response = await fetch('https://api.podpdf.com/templates/YOUR_TEMPLATE_ID/render', {
method: 'POST',
headers: { 'X-API-Key': process.env.PODPDF_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
data: {
invoice_number: order.number,
issue_date: order.date, // "2026-09-13"
customer: { name: order.customerName, address: order.address },
items: order.lines.map((line) => ({ description: line.name, quantity: line.qty, unit_price: line.price })),
tax_rate: 20,
},
}),
});

if (response.status === 400) {
const { error } = await response.json();
console.error(error.details.errors); // [{ field: 'items[0].quantity', error: 'must be a number' }]
} else {
const pdf = Buffer.from(await response.arrayBuffer());
}

Write the template yourself in HTML (beta)​

Beta

HTML templates are in beta. The tags and the editor may still change, and we'd like to hear what's missing.

If you already have your document as HTML, you don't have to rebuild it from blocks. On the Templates page click New HTML template. It opens a code editor with a live preview, starting from a short invoice you can replace.

Write ordinary HTML and CSS, and put a tag where each value from your JSON goes:

<style>
h1 { color: var(--pp-accent); }
td { padding: 2mm; border-bottom: 0.2mm solid #ddd; }
</style>

<h1>Invoice {{invoice_number}}</h1>
<p>Bill to {{customer.name}}</p>

<table>
{{#each items}}
<tr><td>{{@number}}</td><td>{{description}}</td><td>{{amount}}</td></tr>
{{else}}
<tr><td>Nothing to bill</td></tr>
{{/each}}
</table>

{{#if notes}}<p>{{notes}}</p>{{/if}}
<img src="{{logo}}" alt="">
TagWhat it prints
{{field}}The value, formatted by the field's type (money, dates, numbers) in the template's language and currency
{{#each list}}…{{else}}…{{/each}}Its content once per row of a list field. Inside it, the list's columns are available by name, {{@number}} counts the rows, and {{@root.field}} reaches a field outside the list. The {{else}} part is printed when there are no rows.
{{#if field}}…{{else}}…{{/if}}Its content only when the field has a value
<img src="{{logo}}">An image field (https:// URL or data:image/…)
<img src="{{asset:…}}">An image you uploaded with Insert → Upload an image

Fields, test data, page size, Generate PDF and Use via API work exactly as for a visual template, and the render request is the same. Insert in the editor adds a tag for any field, a block for a list, or an image.

What to know before you start:

  • Everything is in the template. Scripts don't run, and nothing is loaded from the internet: no external stylesheets, web fonts or remote images. Put your CSS in a <style> block, upload images or send them as an image field, and use the built-in fonts (Inter, Roboto, Merriweather, Roboto Mono, Great Vibes). The editor points out anything that won't appear in the PDF.
  • Values are always printed as text. HTML in your data is shown as written, never run. A tag can go in text or inside a quoted attribute (class="{{status}}", style="color: {{colour}}"), but not inside a <style> block.
  • Links must be web, email or phone addresses. Anything else is left out of the PDF.
  • Totals are yours to send. An HTML template prints the values it is given; it doesn't add up line items the way the visual builder's totals block does.
  • No barcodes or QR codes yet. Use a visual template for those, or send the code as an image.
  • Limits: 200 KB of HTML per template, and a rendered document of up to 12 MB.
  • A template stays the kind it was created as. To move a visual template to HTML, create a new HTML template.

Tips​

  • Receipts: choose Receipt 80 mm or Receipt 58 mm. The page grows with the number of items, so a single roll-paper page is always produced.
  • Labels: the Shipping label 4 × 6 in starter includes Code 128 and QR codes generated from your tracking number.
  • QR codes: QR code only (50 mm square) prints just a code from any URL or text you send as qr_value. The QR code poster, Wi-Fi QR card, name badge and event ticket starters build codes from their fields too — for example the Wi-Fi card encodes WIFI:T:{{security}};S:{{network}};P:{{password}};;.
  • Retail and inventory: the product price label prints an EAN-13 from 12 or 13 digits, and the asset tag prints a Code 128 and a QR code.
  • Long invoices: table headers repeat on every page, rows never split across pages, and the footer can show "Page 2 of 3".
  • Logos: upload a PNG, JPEG or SVG (up to 1 MB) in an image block, or send an https:// image URL in your data.
  • Changing a template does not change its ID, so your integration keeps working. Use Duplicate to try a new layout without affecting live traffic.

See the Templates API reference for every option and error code.