Blog/Use cases

Generate invoice PDFs from HTML

Use cases··5 min read

To generate an invoice PDF from HTML, POST /v1/pdf with exactly one source: html for a one-off document you already built, or templateId plus templateData for a published Handlebars layout. HTML is a UTF-8 JSON string, not Base64. The host is https://api.relaypdf.com. Auth is a bearer key. That is the generate-invoice-pdf-api path. The product pattern lives at https://relaypdf.com/use-cases/invoices. This post is the copy-paste vs templateId split, not a rewrite of that page.

Two inputs, one endpoint

POST /v1/pdf prints HTML, a public URL, GitHub-flavoured Markdown, or a published Handlebars template through Chromium. You send one of html, url, markdown, or templateId. Mixing sources in one request is not a documented option. Office files go to POST /v1/convert. Stitching an existing cover PDF onto an invoice is POST /v1/pdf/merge. Both of those are later steps, not the render.

html is the right field when your billing code already emits a complete invoice document — server-rendered HTML, a string from a job, a one-off you are debugging. templateId is the right field when the layout is stable and the payload is data: invoice number, parties, line items, totals. The template API is at https://relaypdf.com/docs/templates. The print contract is at https://relaypdf.com/docs/pdf.

You haveSend
A finished invoice HTML stringhtml
A published Handlebars invoicetemplateId + templateData
A public invoice pageurl (host must be public)
A Markdown draft, not a ledgermarkdown (GFM → HTML → Chromium)

Markdown will print. It will not give you a pixel-aligned two-column ledger. If the invoice has to look like the charcoal masthead or the amber spine folio, use HTML or a cloned gallery template.

Copy-paste HTML

The HTML-to-PDF product page uses an invoice as the sample. Send html, an optional filename that ends in .pdf, and Chromium options. printBackground defaults to true, which matters if the amount-due block is a filled rule. format defaults to letter; European invoices usually want A4. Field names are the same in JSON, Node, and Python.

curl https://api.relaypdf.com/v1/pdf
  -H "Authorization: Bearer pdf_live_..."
  -H "Content-Type: application/json"
  -d '{
    "html": "<!doctype html><html><body><h1>Invoice INV-1042</h1><p>Total: $1,458.00</p></body></html>",
    "filename": "invoice-inv-1042.pdf",
    "options": {
      "format": "A4",
      "printBackground": true,
      "margin": { "top": "12mm", "right": "12mm", "bottom": "12mm", "left": "12mm" }
    }
  }'
  --output invoice-inv-1042.pdf

Default response is application/pdf with x-relaypdf-id, x-relaypdf-size, and content-disposition. For n8n, Make, or a worker that only wants a URL, set response to url. You get JSON: id, status, a public file URL under /v1/files/..., filename, sizeBytes, expiresAt (24 hours). response: async returns 202; poll GET /v1/jobs/:id or set callbackUrl to an HTTPS endpoint. Failed renders return render_failed and are not billed. options.timeout is the Chromium budget, max 60000 ms, not the SDK HTTP timeout.

If the HTML loads a webfont or a QR image, waitUntil defaults to networkidle0. That is usually enough. waitForSelector and waitForTimeout (max 30000 ms) exist if a late chart still prints empty. extraHTTPHeaders and cookies apply to url fetches, not to an html body you already assembled.

Keep CSS page-break rules in the HTML if line items wrap. break-inside: avoid on a row is a document problem, not an API flag. headerTemplate and footerTemplate are Chromium margin fragments, documented on https://relaypdf.com/docs/options. They do not clone a <header> from the body. Sibling for the chrome: https://relaypdf.com/blog/html-to-pdf-headers-footers. The raw HTML route: https://relaypdf.com/html-to-pdf.

Handlebars: clone, publish, send data

Use a stored template when the same invoice layout will render many times. Lifecycle is draft → publish → render. You can keep editing a draft while a published version stays live. templateId may be the UUID or the slug. Optional templateVersion pins a published version; omit it for the latest published. strict: true fails the job if a Handlebars path is missing. Request options override the options saved on the template for that job.

The public gallery is the same catalog the dashboard uses. GET /v1/templates/gallery lists cloneable layouts. Public pages live at /pdf-templates and /pdf-templates/:category/:id. One invoice that is live today is the charcoal masthead with a payment QR: https://relaypdf.com/pdf-templates/invoice/charcoal-masthead-invoice-with-payment-qr. The B&W itemized layout is https://relaypdf.com/pdf-templates/invoice/invoice. Clone in the dashboard or POST /v1/templates with galleryId.

curl https://api.relaypdf.com/v1/templates
  -H "Authorization: Bearer pdf_live_..."
  -H "Content-Type: application/json"
  -d '{
    "name": "Invoice Template",
    "galleryId": "invoice"
  }'

That creates an account-owned draft. Publish it (POST /v1/templates/:id/publish) before you render. Then POST /v1/pdf with templateId and templateData. The docs sample is the shape:

curl https://api.relaypdf.com/v1/pdf
  -H "Authorization: Bearer pdf_live_..."
  -H "Content-Type: application/json"
  -d '{
    "templateId": "invoice",
    "templateData": { "number": "INV-1042", "total": 1458 },
    "filename": "invoice-inv-1042.pdf",
    "response": "url"
  }'

Real gallery invoices expect more than number and total. The published Invoice Template sample includes issuedOn, dueOn, currency, tax, parties, items[], and a payment object (url, bank, terms). Match the template’s sampleData. If you clone charcoal, use that page’s sample, not the B&W one. POST /v1/templates/validate and POST /v1/templates/preview exist for HTML plus sampleData before you publish.

Python

pip install relaypdf (0.1.2 as of 22 Aug 2026). from_html and from_template both hit POST /v1/pdf. Option keys stay camelCase.
from relaypdf import RelayPDF
client = RelayPDF(api_key="pdf_live_...")

# One-off HTML

pdf = client.pdf.from_html(
    "<h1>Invoice INV-1042</h1><p>Total: 1458</p>",
    filename="invoice-inv-1042.pdf",
    options={"format": "A4", "printBackground": True},
)
pdf.save("invoice-inv-1042.pdf")

# Published template

pdf = client.pdf.from_template(
    "invoice",
    {"number": "INV-1042", "total": 1458},
    filename="invoice-inv-1042.pdf",
    response="url",
)

After the PDF exists

The invoices use-case lists the extras without making them the render: response:url for chaining, optional merge of a cover or terms PDF via POST /v1/pdf/merge, optional /v1/zip if you need the invoice plus supporting files in one archive. In n8n, n8n-nodes-relaypdf has a Source Template action for the published path. None of that is e-sign. RelayPDF does not add a signature ceremony to an invoice.

Do not send html and templateId together. Do not Base64 the HTML. Do not put the API key in a query string. filename must end with .pdf. Private, loopback, and metadata hosts are rejected on url; if the invoice sits behind login, render HTML in your app and send html.

If you are still choosing between pasting HTML and publishing a template: paste until the CSS is stable, then clone a gallery invoice, publish, and send templateData. Start with the charcoal layout if you want a payment QR in the footer rail, or the B&W Invoice Template if you want a two-column ledger. Pattern page: https://relaypdf.com/use-cases/invoices. Gallery: https://relaypdf.com/pdf-templates. Template API: https://relaypdf.com/docs/templates. Print: https://relaypdf.com/docs/pdf.

Ready to generate?

One API for HTML, Markdown, URLs, and Office. REST, SDK, CLI, or MCP.