Blog/Guides

Handlebars PDF templates from JSON

Guides··5 min read

Store a Handlebars layout once, publish it, then print a PDF with POST /v1/pdf and a JSON templateData object. That is the handlebars pdf template api on RelayPDF. templateId is the published UUID or slug. Optional templateVersion pins a published version. Optional strict: true fails the job when a Handlebars path is missing. Docs: https://relaypdf.com/docs/templates. Stock layouts: https://relaypdf.com/pdf-templates.

When a template beats inline HTML

POST /v1/pdf accepts exactly one source: html, url, markdown, or templateId. One-off markup still belongs on html. Use a stored template when the same invoice, letter, or report will render many times with different data. HTML in drafts is a UTF-8 JSON string, not Base64. The print engine is Chromium, same as html and url jobs.

Do not send html and templateId together. That is invalid_request. Do not treat a draft as renderable. The render path requires a published version. You can keep editing a draft while the last published version stays live.

Draft, then publish

Create a draft with POST /v1/templates. The documented engine is handlebars. Read or change the draft with GET or PATCH /v1/templates/:id. DELETE /v1/templates/:id soft-deletes the record. GET /v1/templates lists what you own. GET /v1/templates/:id returns the draft if one is present, otherwise the published copy.

POST /v1/templates/:id/publish publishes the active draft. An optional comment is accepted on the Python helper publish(id, comment=None). After publish, POST /v1/pdf can take that templateId. POST /v1/templates/:id/discard drops unpublished draft changes. POST /v1/templates/:id/duplicate clones the template. GET /v1/templates/:id/versions is history. POST /v1/templates/:id/restore restores a version.

Validate and preview before you publish

POST /v1/templates/validate checks HTML plus sampleData without publishing. POST /v1/templates/preview runs a Chromium preview of unsaved HTML. Use those when you are iterating in the dashboard or from CI. They are not substitutes for a published templateId on /v1/pdf.

POST /v1/templates/generate accepts a prompt and returns generated html, sampleData, and options. Successful AI generations debit the AI template rate ($0.05 at launch). Failures are not billed. ai_unavailable is a 503 and is not billed.

Clone a stock layout

GET /v1/templates/gallery returns cloneable stock layouts. Public catalog pages live at /pdf-templates and /pdf-templates/:category/:id. The live gallery lists 37 templates in 33 categories (invoices, packing slips, quotes, certificates, resumes, labels, NDAs, reports, tickets, and others). Clone into your account, publish, then render with your own templateData. SDK: client.templates.gallery().

Render: templateId plus templateData

Bearer key. JSON body. Host https://api.relaypdf.com. The documented invoice sample is the shortest render:

curl -X POST 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.pdf"
  }'
  --output invoice.pdf

templateData is a JSON object injected into Handlebars. Keys must match the paths the layout reads. filename, if sent, must end with .pdf. response is binary (default), url, or async. callbackUrl, if present, must be https. Request options override saved PDF options for that job only.

Pin a published version with templateVersion (integer on OpenAPI CreatePdfRequest). Omit it and the job uses the latest published version. Set strict to true when missing Handlebars paths should fail instead of printing empty tokens.

Pin a version

{
  "templateId": "invoice",
  "templateVersion": 2,
  "templateData": { "number": "INV-1042", "total": 1458 },
  "strict": true,
  "filename": "invoice.pdf"
}

Pin when finance or legal already approved a published layout and you are still editing the next draft. Restore a prior version with POST /v1/templates/:id/restore if you need that copy to become current again. Version numbers come from GET /v1/templates/:id/versions; do not invent a numbering scheme.

SDK, CLI, MCP

JSON field names stay camelCase in every official client: templateId, templateData, templateVersion, strict. Node: client.pdf.fromTemplate. Python 0.1.2: client.pdf.from_template(templateId, templateData, **extra). Documented Python call:

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

Python templates helpers on 0.1.2: list, gallery, get, create, update, delete, publish. Node also documents discard, duplicate, versions, restore, validate, preview, and generate. Those REST paths exist if you need them before a Python minor. CLI: relaypdf templates list|create|publish, and relaypdf pdf --template. MCP uses the same bearer key. Run npx @relaypdf/cli setup. Do not ask a human to paste a key. Query-string keys are rejected.

Response modes and debit

binary returns application/pdf with x-relaypdf-id, x-relaypdf-size, and content-disposition. url returns JSON with id, status, url, filename, sizeBytes, and expiresAt — a public GET /v1/files/:id good for 24 hours, no key on download. async returns 202 with a pollUrl; poll GET /v1/jobs/:id until completed or failed.

JobEndpointTypical debit
Template PDFPOST /v1/pdf + templateId$0.015
HTML / URL / Markdown PDFPOST /v1/pdf$0.015
AI template generatePOST /v1/templates/generate$0.05
ScreenshotPOST /v1/images$0.015

Failed jobs, 429s, and 402s are never billed. Trial wallets: 20 requests/minute. Funded or auto-reload: 60/minute. Burst: 5 / 10 seconds. GET /v1/account returns plan, rateTier, and wallet.balanceMillicents and is not billed. Ledger unit is millicents ($0.001). $5.00 trial is 5000 millicents.

Errors you will actually see

invalid_request for exclusive sources or a bad body. unauthorized for a missing or unknown key. payment_required when the wallet is empty. rate_limited with Retry-After. not_found for an unknown template, job, or file. payload_too_large if HTML is oversized (no published megabyte figure). render_failed if Chromium could not print. Branch on error.code in { error: { code, message } }.

What this is not

This is not an AcroForm fill. Filling an existing PDF is POST /v1/pdf/form/fill. This is not Office mail-merge; .docx and .xlsx go to POST /v1/convert. This is not a screenshot; bitmaps are POST /v1/images. RelayPDF does not claim OCR, e-sign, PDF/A, or HIPAA on this path. A Handlebars template is HTML plus data, then Chromium print CSS.

Invoice-shaped stock layouts and a JSON payload are the usual first ship. Sibling walkthrough for that use case: https://relaypdf.com/blog/generate-invoice-pdf-api.

Ship it

Create a draft, validate HTML and sampleData, publish, then POST templateId and templateData to https://api.relaypdf.com/v1/pdf. Pin templateVersion when a published layout must not drift. Set strict when missing paths should fail. Field list: https://relaypdf.com/docs/templates. Gallery: https://relaypdf.com/pdf-templates. POST /v1/pdf fields: https://relaypdf.com/docs/pdf. OpenAPI: https://relaypdf.com/openapi.json.

Ready to generate?

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