When the same document goes out again and again with different details, such as invoices, packing slips, certificates or form letters, the layout should be written once and the data should change. This generator works that way. You keep an HTML template with placeholders like {{customer.name}}, paste the data as JSON, and get a finished PDF. Change the JSON and the document changes with it, with no copy and paste into a word processor.
The template language is a small, safe version of Mustache and Handlebars, the syntax most developers already know. Values are HTML-escaped by default, loops repeat rows for every item, and filters format money and add up totals. The template is filled in your browser; only the finished HTML goes to our server, where the same engine as our HTML to PDF converter draws the PDF.
How to make a PDF from JSON
- Choose a starter under Start from a template: Invoice, Receipt, Certificate, Shipping label, Letter or Blank. Each one loads a template, sample data and page settings that suit it.
- Edit the Template box. It is ordinary HTML and CSS with placeholders in double curly braces.
- Replace the sample in the JSON data box with your own data, or click Open a .json file. Format JSON tidies the indentation.
- Read the two status lines. Under the data box you see Valid JSON or the exact line and column of an error. Under the template box you see Template OK, a syntax error with its line number, or a list of placeholders that were not found in your data.
- Check the Live preview. It redraws as you type, at the printable width of the page size and margins you chose.
- Pick a page size (A4, Letter, Legal or 4 x 6 in), orientation and margins, and a file name if you do not want the one taken from the template's <title>.
- Click Download PDF, or Download HTML to keep the filled page, or Print with my browser to save it through your browser's print dialog.
Your template and data are saved in this browser for each starter, so you can close the tab and continue later. Reset this template brings back the original.
The starter templates
| Starter | Page setup it loads | What the data holds |
|---|---|---|
| Invoice | Portrait, 15 mm margins | Seller, customer, invoice number and dates, line items with quantity and price, discount, tax rate, notes; subtotal, tax and total are calculated |
| Receipt | Portrait, 15 mm margins, narrow centered slip | Store, receipt number, date, items, tax rate and payment method; totals are calculated |
| Certificate | Landscape, 10 mm margins | Organization, recipient, course, hours, date, signers and a certificate ID |
| Shipping label | 4 x 6 in, 5 mm margins | Service, sender and recipient addresses, order number, weight, item list and tracking number |
| Letter | Portrait, 20 mm margins | Letterhead, date, recipient, subject, a list of paragraphs and the sign-off |
| Blank | Portrait, 15 mm margins | A title and a short list, as a clean starting point |
The page size starts as Letter if your browser language is set to a region that uses Letter paper, such as the US or Canada, and A4 elsewhere; the label always starts at 4 x 6 in. All starters lay out columns with tables, because the PDF engine does not support flexbox or grid, so what you see in the preview is what you get in the PDF.
Template syntax
The full reference is under Template syntax in the tool. These are the parts you will use most.
Values
{{name}} inserts a value. Dots reach into nested objects: {{customer.address}}. Characters such as < and & are escaped, so a customer called "Smith & Sons" prints correctly and data can never inject markup into your layout. A value that is missing prints as blank, and the template status line lists it so you can spot a typo.
Triple braces insert raw HTML: {{{notes_html}}}. Use them for data that already contains formatting, such as a bold word or a line break. Script tags and event handlers are removed from the finished HTML in the preview, in browser printing and on the server, so raw HTML can style the page but cannot run code.
Loops and conditions
{{#each items}} ... {{/each}} repeats its content for every item in a list. Inside the loop, the item's own fields are available by name, {{this}} is the whole item (useful for lists of plain text), {{@number}} counts from 1 and {{@index}} from 0. Fields from outside the loop are still found, so {{currency}} works inside it; {{../name}} reaches one level out when the item has a field with the same name. An {{else}} inside a loop shows when the list is empty.
{{#if paid}} ... {{else}} ... {{/if}} shows a block when the value is present and not false, 0, an empty string or an empty list. {{#unless}} is the reverse, and {{#with customer}} lets you write {{name}} instead of {{customer.name}} inside the block. {{! comment }} is left out of the output.
Filters for money and totals
A filter follows a pipe and can take arguments after colons. Arguments can be quoted text, numbers or field names.
| Write | With this data | Prints |
|---|---|---|
{{price|money:"$"}} | price: 1234.5 | $1,234.50 |
{{qty|times:price|money:currency}} | qty: 12, price: 18.5, currency: "$" | $222.00 |
{{items|sum:"qty*price"}} | two items, 2 x 1.10 and 3 x 0.70 | 4.3 |
{{items|count}} | a list of 2 items | 2 |
{{subtotal|add_percent:tax_rate}} | subtotal: 100, tax_rate: 8.25 | 108.25 |
{{status|upper}} | status: "paid" | PAID |
{{phone|default:"n/a"}} | phone missing | n/a |
The math filters are times, divide, plus, minus, percent (x percent of the value), add_percent and round. The text filters are upper, lower, title, default, join and number. For conditions there are eq, ne, gt and lt, so {{#if status|eq:"paid"}} shows a PAID stamp only on paid invoices. Money is rounded to the cent when it is printed, not before, so totals match what a spreadsheet would give.
Worked example: the invoice totals
The invoice starter has three lines: 12 linen throws at $18.50, 24 mugs at $6.25 and 18 candles at $7.40, with a $25 discount and 5.5% tax. The template never stores a total. It calculates them:
- Each line:
{{qty|times:price|money:currency}}gives $222.00, $150.00 and $133.20. - Subtotal:
{{items | sum:"qty*price" | money:currency}}gives $505.20. - Tax: subtotal minus discount, then 5.5 percent of it:
{{items | sum:"qty*price" | minus:discount | percent:tax_rate | money:currency}}gives $480.20 x 5.5% = $26.41. - Total due:
{{items | sum:"qty*price" | minus:discount | add_percent:tax_rate | money:currency}}gives $506.61.
Add a fourth item to the JSON and every figure updates in the preview before you download. Change "currency": "$" to "โฌ" and every amount changes symbol. If you sell on Shopify and only need a standard invoice, the Shopify invoice generator is a form with no template to edit.
Fixing JSON and template errors
JSON is strict, and most errors come from a handful of habits carried over from JavaScript. The status line under the data box names the line and column and shows the line with a marker under the problem.
| Message | Usual cause | Fix |
|---|---|---|
| A property name needs double quotes, or there is an extra comma | {'name': 1} or a comma before } | Use "double quotes" and remove the last comma in a list or object |
| A comma is missing between two values | Two lines without a comma between them | Add a comma at the end of the line above the marker |
| The JSON ends too early | A missing } or ] | Count your brackets; Format JSON helps once it parses |
| A text value contains a real line break | Pressing Enter inside quotes | Write \n inside the quotes instead |
Template errors are reported the same way, with a line number: a {{#each}} that is never closed, a {{/if}} that closes the wrong block, or a filter name that does not exist. While there is an error, the preview keeps showing the last version that worked, so you do not lose your place.
Designing your own template
The fastest way to a custom template is to start from the closest starter and change it, because the starters already solve the print problems. If you write one from scratch, these rules keep the preview and the PDF in step:
- Build side-by-side areas with tables. A two-cell table with
width: 100%gives you a logo on the left and invoice details on the right on every page size. - Put all CSS in a <style> block in the template. Placeholders work inside CSS too, so
color: {{brand_color | default:"#0f3d5e"}}lets each document carry its own brand color. - Give every field a sensible fallback with
default, so a missing phone number prints n/a instead of an empty gap. - Wrap optional parts in
{{#if}}. The invoice starter hides the discount row and the notes box when the data has none. - Keep one row per item in loops. Because a table row cannot split across pages, a loop of short rows flows onto page two cleanly, while one giant row cannot.
- Set the <title> from your data, for example
Invoice {{invoice.number}}. It becomes the file name, which saves renaming every download.
Test the template with awkward data before you rely on it: a long company name, an address with an apostrophe, twenty line items and a list with no items at all. If all four look right in the preview, they will look right in the PDF.
Where the JSON comes from
Most systems can hand you JSON. Store and marketplace APIs return orders as JSON with a list of line items, form tools and spreadsheets can export rows as JSON, and developers can print it from their own code. You do not need to change the data to fit the starter: rename the placeholders in the template to match your field names instead. For example, if your order data has a line_items list whose items have title, quantity and price, change the loop to {{#each line_items}} and the cells to {{title}}, {{quantity}} and {{quantity|times:price|money:"$"}}.
If your catalog data lives in a store you manage with AM Jarvis, the Product Importer keeps product titles, variants and prices consistent, which makes the documents you generate from that data consistent too.
PDF, HTML or browser print
- Download PDF sends the filled HTML to our server, which draws a vector PDF with selectable text. It counts toward the daily free limit shared with the HTML to PDF converter.
- Download HTML saves the filled page with its page-size rule, entirely in your browser. Open it later, email it, or keep it as a record of exactly what was sent.
- Print with my browser opens your browser's print dialog for the filled page, with scripts disabled. Choose Save as PDF. Use it when you have written a layout with flexbox or grid, which the server engine does not support.
Limitations
- One document per download. There is no batch mode that makes a PDF for every record in a list; for that, loop over the records in your own code and post each one.
- The filled HTML must stay under 2 MB and about 8,000 elements, the same limits as the HTML to PDF converter.
- Filters cover formatting and simple math. There are no custom functions, partials or date formatting, so format dates in your data the way you want them printed.
- Images must be https addresses or data: URIs. Inline <svg> markup does not render in the PDF; link the SVG as an image instead.
- The shipping label prints the tracking number as text. It does not generate a carrier barcode; make one with our barcode generator and insert it as an image.