Blog/Use cases

Generate Invoice PDFs from HTML

Build an invoice from HTML and data, control print layout, and return a PDF through the API.

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 $RELAYPDF_API_KEY" \
  -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 $RELAYPDF_API_KEY" \
  -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 $RELAYPDF_API_KEY" \
  -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

Load the key from the environment created by relaypdf setup, then use the Python SDK:

import os
from relaypdf import RelayPDF
client = RelayPDF(api_key=os.environ["RELAYPDF_API_KEY"])

# 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.

For a complete worked example, validate data extracted from an existing invoice, or generate invoices in ASP.NET Core, or generate invoices in Spring Boot.

Ready to generate?

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