HTML to PDF in n8n
This is one of the most-asked questions in the n8n community, and the first answer people find is wrong. There are three real ways to turn HTML into a PDF inside a workflow. This page describes all three, including the two that do not involve us, and says what each does with the binary afterwards — which is the part that actually costs you an evening.
1. A built-in node — it does not exist #
The Convert to File node handles CSV, JSON, XML, spreadsheets, text and base64. It does not render HTML, and it does not produce PDFs. Neither does Extract from File, which reads PDFs rather than writing them. There is no core n8n node that renders a web page.
The reason is not an oversight: rendering HTML faithfully means shipping a browser. n8n would have to bundle Chromium into every installation to do what a one-line HTTP call can do instead. So every real answer is one of the next two.
2. An HTTP Request node against any PDF API #
This works today, on Cloud and self-hosted, with no installs and no community-node policy to think about. It is the right answer if you already pay for a PDF API, or if your organisation does not allow community nodes.
The one thing to get right is the response format. By default n8n will try to parse the response as JSON and you will get mojibake instead of a file. Set Response → Format → File and name the output binary property.
Method: POST
URL: https://pdf.mintapis.com/v1/pdf
Authentication: Generic Credential Type → Header Auth
Name: Authorization
Value: Bearer pm_live_...
Body: JSON
{"html": "{{ $json.html }}", "format": "A4"}
Response:
Format: File
Output field: dataWhat you give up doing it this way: the response headers. PDFMint returns
X-PDFMint-Pages, X-PDFMint-Duration-Ms,
X-PDFMint-Credits-Remaining and X-PDFMint-Warning, and an HTTP Request node
in File mode drops them. If you never branch on page count or warnings, you will not miss them.
3. A community node #
A dedicated node exists so that the fiddly parts — binary handling, the headers above,
per-item execution, and a credential that tests itself — are configured rather than rebuilt.
n8n-nodes-pdfmint is a thin client over the same HTTP API, with zero runtime
dependencies, built and tested against n8n 2.35 on Node.js 20 and 22, and marked
usableAsTool so an AI Agent node can call it directly.
It is verified by n8n: the package is in n8n's community-node registry with
isPublished: true, which is the list n8n Cloud installs from. You can confirm that
without an account — the entry is in the response of
https://api.n8n.io/api/community-nodes. We also publish the package: PDFMint and this node are built and run by the same people, so the node and the API it calls stay in step.
One thing to check after installing. n8n Cloud serves
0.4.5; npm's current release is 0.4.6. Both default to
https://pdf.mintapis.com and include the error-output fix. The npm release also adds
the account-free demo to its package documentation. If an
older workflow still shows 0.1.0, update it rather than assuming it updated itself. The registry
and npm evidence is linked on the
docs page.
| Operation | Calls | Notes |
|---|---|---|
| Generate PDF | POST /v1/pdf | Source: HTML, Markdown, a URL or a saved template. Runs once per input item. |
| Generate Image | POST /v1/image | PNG or JPEG screenshot of the same markup. |
| Merge PDFs | POST /v1/merge | Runs once for the whole branch, not once per item. |
| Get Usage | GET /v1/me | Plan, quota and documents remaining, on json. |
Install and credential #
Cloud or self-hosted: Settings → Community Nodes → Install, then enter
n8n-nodes-pdfmint. On self-hosted you can also install it from the CLI:
cd ~/.n8n/nodes npm install n8n-nodes-pdfmint # then restart n8n
The package carries an npm provenance attestation, so npm audit signatures can
verify it was built by the GitHub Actions workflow in the repository, from a specific commit. That
is worth doing for any community node before you let it hold a credential.
Then add a PDFMint API credential and paste a key from
the dashboard — it starts with pm_live_. The credential
tests itself with GET /v1/me, so n8n tells you immediately whether the key works
instead of failing on the first real run.
Where the file ends up — the part that decides everything downstream #
With the default File (Binary) output, the PDF is attached to the output item's
binary field (data, unless you rename it), so Gmail, Google Drive, S3, Slack, Nextcloud
and HTTP Request nodes can consume it directly. There is no download step and no base64 dance.
Alongside it, on json:
| Field | What it tells you |
|---|---|
fileName, mimeType, size | the usual, for the next node |
pages | page count — useful for “this invoice should be one page” |
durationMs | how long the render took |
creditsRemaining | so a workflow can alert you before it runs out |
warning | present when the render succeeded but was probably not what you meant |
Choose Hosted URL or Base64 in JSON instead and the item carries the API's JSON response with no binary attached — the right choice when the next step is a webhook or a database row rather than an attachment.
Three workflows people actually build #
Two of those three are published n8n templates you can import instead of building.
Generate PDF invoices from webhook data
is the first shape: a webhook takes the customer and the line items, the arithmetic happens in one Code node, and a broken
payload gets a 422 with the reason rather than an invoice that says undefined.
Send branded PDF receipts for Stripe payments
carries a checkout.session.completed event through to a receipt in Gmail and Drive. Both are ours, both are free,
and both need self-hosted n8n, because they use a community node.
When the data arrives as an email rather than a webhook, the missing step is in front of PDFMint, not after it. MailMint is ours and does that half: an inbound address or an IMAP mailbox goes in, structured JSON comes out, with the message’s own SPF, DKIM and DMARC verdict attached — so the invoice mail becomes the input of a Set node instead of a regex over the body. Same caveat as everything else on this page: community node, self-hosted n8n, and n8n’s verification of it is still pending.
For the template case, store the layout once with PUT /v1/templates/<name> and
then send only data from the workflow. That keeps a 300-line invoice out of a Set node, where it is
unreadable and undiffable.
When it goes wrong #
Four failures cover almost everything, and each has a named error rather than a 500:
| Symptom | What it means |
|---|---|
unresolved_placeholders | A template placeholder had no value in data. The message names the field. Set strict: false to render anyway. |
A warning on json, PDF looks empty | The page rendered before its content existed — a JavaScript chart, a late image. Use waitFor with a selector. |
| Fonts look wrong | A web font failed to load and the browser substituted silently. Reference it by absolute URL, or inline it. |
| Node output is text, not a file | You are on the HTTP Request path and the response format is not set to File. |
Failed renders refund the credit before the error reaches the workflow, so a retry loop on a broken template does not quietly spend your quota.
What it costs #
One credit per successful document, whatever the page count and whatever the file size. The free tier is 10 documents a month with no card — enough to wire the node up and see a real PDF, not enough to run a daily workflow, which needs Starter at $9 for 5,000 documents. Every feature is on every plan including free: password protection, headers and footers, page numbers, custom page sizes, watermarks, merging, images and hosted links.
How that compares with the other HTML-to-PDF APIs, with every figure read off the vendor's own page: the comparison page.
What we cannot claim #
PDFMint has paying customers, but no customer logo, testimonial or case study that we have
permission to publish. The 1,002 weekly figure is npm downloads — machines running
npm install, including CI and scanners — not users. n8n Cloud serves
0.4.5, while npm serves 0.4.6; the node code is unchanged between them.
PDFMint
runs as a single service in one region with no SLA; its
status page is generated from its own request log and its uptime figure resets
on every deploy, which it says on the page.
Try it #
Install the node, paste a key, drop it after any node that produces HTML, and run once. If it does what you need, the rest of this page was unnecessary; if it does not, the API reference has every option and every error code.
Get an API key The node, in the reference Live latency and success rate