render-bolt
Dashboard

How to turn an HTML invoice into a PDF with one API call

Keep your invoice as an HTML template, send it to /v1/pdf and get back an A4 or Letter PDF with page numbers, the right fonts and clean page breaks.

Most teams already have their invoice as HTML. It is what the customer sees in the dashboard or in the email. Rebuilding it in a PDF library means a second layout that slowly drifts from the first. Rendering the same HTML in a real browser keeps one template, and you get full CSS, web fonts and every alphabet your customers write in.

This guide walks through a production setup: the request, page size and margins, page numbers, fonts, page breaks and where to keep the file.

The request#

Send your HTML in the html field. The response body is the PDF.

curl https://rendersbolt.com/v1/pdf \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>Invoice #1042</h1><p>Total: €120.00</p>","format":"A4","margin":"15mm"}' \
  -o invoice-1042.pdf

The same call from Node.js:

import { writeFile } from 'node:fs/promises';

const html = renderInvoice(order); // your existing template

const res = await fetch('https://rendersbolt.com/v1/pdf', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ html, format: 'A4', margin: '15mm' }),
});
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message}`);
}
await writeFile(`invoice-${order.id}.pdf`, Buffer.from(await res.arrayBuffer()));

If the invoice already has a public URL, send url instead of html. Pages behind a login can't be loaded, so for private documents send the HTML.

Page size and margins#

format accepts A4 (the default), Letter, Legal, A0 to A6, Tabloid and Ledger. Add "landscape": true for wide reports.

For receipts and labels, set width and height instead of format:

{ "html": "...", "width": "80mm", "height": "200mm", "margin": "4mm" }

margin takes one value for all sides ("15mm") or one per side: {"top":"25mm","bottom":"20mm","left":"12mm","right":"12mm"}. Units can be mm, cm, in or px.

If your stylesheet already says @page { size: A5; margin: 12mm }, send "prefer_css_page_size": true and your CSS wins.

header_template and footer_template are small HTML snippets printed on every page. Elements with these classes are filled in: pageNumber, totalPages, date, title and url.

{
  "html": "...",
  "format": "A4",
  "margin": { "top": "20mm", "bottom": "20mm", "left": "12mm", "right": "12mm" },
  "footer_template": "<div style='font-size:9px;width:100%;text-align:center;color:#555'>Page <span class='pageNumber'></span> of <span class='totalPages'></span></div>"
}

Three things trip people up here:

  • The templates are printed inside the page margin. With a bottom margin of zero there is no room, and the footer disappears. If you send a template without any margin, a 20 mm top and bottom margin is used.
  • The templates don't see your page's CSS. Put the styles inline and always set a font-size, because the default is tiny.
  • Images in the header or footer don't load from the network. If you need a logo there, inline it as a data: URI.

Fonts and characters#

The renderer ships Noto fonts for Latin, Turkish, Arabic, Hebrew, Chinese, Japanese, Korean, Thai and Devanagari, plus color emoji. Names like Şükrü Öztürk or 王小明 come out as text, not as empty boxes.

For your brand font, load it in the HTML with @font-face or a Google Fonts link. The renderer waits for web fonts before it prints. If your fonts come from a slow host, also send "wait_until": "networkidle" to give them more time.

Page breaks for long invoices#

Long tables are where PDFs usually go wrong. A few CSS rules handle most of it:

thead { display: table-header-group; }   /* repeat the table head on every page */
tr, .totals { break-inside: avoid; }     /* never split a row or the totals box */
.terms { break-before: page; }           /* start the terms on a new page */

PDFs are printed with emulate_media: "print" by default, so your @media print rules apply. Backgrounds are printed too (print_background is true), which keeps colored headers and striped rows. To get the page exactly as it looks on screen, send "emulate_media": "screen".

By default the PDF is the response body, which is what you want when you save it to your own storage. If you need a link to email or to hand to another service, ask for "response_type": "url" and you get JSON back:

{
  "url": "https://rendersbolt.com/v1/files/....pdf",
  "expires_at": "2026-10-30T09:12:44.000Z",
  "content_type": "application/pdf",
  "size": 48213,
  "pages": 2
}

The link works for 24 hours, then the file is deleted. For invoices you are required to keep, save the file in your own storage.

A checklist before you ship#

  • Render the PDF once, when the invoice is final, and store it. Don't render on every download.
  • Send "cache": true if the same HTML may arrive twice in a short time. Identical requests then return the stored result for cache_ttl seconds.
  • Handle 422 errors: they mean the page, or a resource it needs, could not be loaded. Failed renders don't count toward your quota.
  • Test with your longest real invoice, not only with a one-line sample.

Every option is listed in the PDF reference.

Get your free API key

100 renders a month with every feature. No card needed.