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 have | Send |
|---|---|
| A finished invoice HTML string | html |
| A published Handlebars invoice | templateId + templateData |
| A public invoice page | url (host must be public) |
| A Markdown draft, not a ledger | markdown (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.