ScreenshotNeo

BlogHow-to

How to Convert HTML to PDF in n8n

Build an n8n workflow that turns complete HTML into a reliable PDF with Gotenberg, hosted APIs, troubleshooting, and a browser-free alternative.

By the ScreenshotNeo team1 October 20268 min read

Direct answer: In n8n, create a complete HTML document, convert it to binary data named index.html, send that file as multipart form data to Gotenberg’s POST /forms/chromium/convert/html endpoint, then pass the returned PDF binary to storage, email, a webhook response, or another document step.

The most dependable workflow is:

  1. Generate a full <html>, <head>, and <body> document.
  2. Make every stylesheet, image, font, and script reachable by the renderer.
  3. Turn the HTML string into binary data with the exact filename index.html.
  4. Use an HTTP Request node to upload that binary as multipart form data to Gotenberg.
  5. Keep the HTTP response as binary PDF data and deliver it in a later node.

1. What you need before building the workflow

  • A running n8n instance.
  • A reachable Gotenberg service. The n8n template uses a Docker Compose service based on gotenberg/gotenberg:8, reachable from n8n at http://gotenberg:3000.
  • HTML that is valid enough for Chromium to parse.
  • Network access from the Gotenberg container to any external CSS, image, font, API, or JavaScript resource your page needs.

Self-hosted Gotenberg gives you control over the Chromium service. A hosted API can remove container operations, but you must review its current authentication, limits, terms, and pricing yourself.

2. Build the n8n workflow

Step 1: Create the HTML

Use a Set, Code, or upstream data node to create one complete HTML string. This example uses a Set node field called html and a deterministic file_name.

{
  "html": "<!doctype html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <title>Invoice</title>\n  <style>body{font-family:Arial,sans-serif;margin:40px}h1{color:#17324d}.total{font-size:24px;font-weight:700}</style>\n</head>\n<body>\n  <h1>Invoice #1042</h1>\n  <p>Prepared by the n8n workflow.</p>\n  <p class=\"total\">Total: $125.00</p>\n</body>\n</html>",
  "file_name": "invoice-1042.pdf"
}

When values come from previous nodes, interpolate them carefully and escape user-controlled text before placing it in HTML. Keep CSS in the document while diagnosing layout issues; move it to separate files after the basic conversion works.

Step 2: Convert the string into an index.html binary

The upload field must be named exactly index.html. In a Code node, encode the HTML as a binary property:

const html = $json.html;
if (!html || !html.includes('<html')) {
  throw new Error('Expected a complete HTML document in $json.html');
}

const data = Buffer.from(html, 'utf8');
return [{
  json: {
    file_name: $json.file_name || 'document.pdf'
  },
  binary: {
    'index.html': {
      data: data.toString('base64'),
      mimeType: 'text/html',
      fileName: 'index.html'
    }
  }
}];

If your n8n version uses a different binary helper, create the same result through its binary-data utilities: a binary property called index.html, MIME type text/html, and filename index.html.

Step 3: Configure the HTTP Request node

Configure the node as follows:

Setting Value
Method POST
URL http://gotenberg:3000/forms/chromium/convert/html
Body content type Multipart form-data
Form file field index.html
Binary property index.html
Response format File/binary
Output binary property data (or another name you use consistently)

Gotenberg returns the generated PDF as the response body. You can set a deterministic output name with the Gotenberg-Output-Filename header. Use an expression such as {{$json.file_name}} when your node supports expression values.

Step 4: Deliver the PDF

Connect the HTTP Request node to the destination you need:

  • Cloud storage: select the HTTP node’s PDF binary property.
  • Email: attach the same binary property.
  • Respond to Webhook: return the binary as the response.
  • Another document node: map the binary field without converting it back to text.

3. A complete workflow example

This Code node creates the document and the binary upload in one step, followed by an HTTP Request node configured as above.

const items = $input.all();

return items.map((item, index) => {
  const customer = String(item.json.customer ?? 'Customer');
  const amount = String(item.json.amount ?? '0.00');
  const safeCustomer = customer.replace(/[&<>\"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',"'":'&#39;'}[c]));
  const safeAmount = amount.replace(/[^0-9.\-]/g, '');

  const html = `<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    @page { size: A4; margin: 18mm; }
    body { font-family: Arial, sans-serif; color: #17202a; }
    h1 { margin-bottom: 24px; }
    .amount { font-size: 22px; font-weight: bold; }
  </style>
</head>
<body>
  <h1>Invoice</h1>
  <p>Customer: ${safeCustomer}</p>
  <p class="amount">Total: $${safeAmount}</p>
</body>
</html>`;

  return {
    json: { file_name: `invoice-${index + 1}.pdf` },
    binary: {
      'index.html': {
        data: Buffer.from(html, 'utf8').toString('base64'),
        mimeType: 'text/html',
        fileName: 'index.html'
      }
    }
  };
});

For multiple input items, configure the HTTP Request node to run once per item. Each item then produces one PDF binary.

4. Assets, CSS, fonts, and JavaScript

Relative paths work when the renderer can access the referenced files. A stylesheet, image, or font on your laptop is not automatically available inside a Gotenberg container. Use one of these approaches:

  • Inline small CSS and critical images as data URLs.
  • Serve assets from a hostname reachable by the Gotenberg container.
  • Mount a shared directory when your deployment model supports it.
  • Allow outbound network access for trusted external assets.

Check asset responses when a PDF has missing images, fallback fonts, or unstyled content. Authentication-protected assets need a renderer-accessible authentication method; a URL that works in your browser may still return 401 or 403 to Gotenberg.

JavaScript-rendered pages

Client-rendered values, charts, and external content can be captured before rendering finishes, producing blank or incomplete sections. Add the documented wait or render controls supported by your Gotenberg version, and verify that the data exists in the page before conversion. A fixed delay can help, but a condition tied to a rendered selector is usually easier to reason about.

5. Choosing an HTML-to-PDF route

Route Best fit Trade-offs to review
Gotenberg via HTTP Request You can run a Docker service and want Chromium control You operate the service, networking, updates, and resource limits
n8n HTML-to-PDF integration / PDFMunk You want an integration covering HTML/CSS or URL-to-PDF Verify installation, service terms, and current capabilities in your n8n instance
PDF.co You prefer a hosted API and do not want to operate Chromium Review API credentials, limits, current pricing, and page-load options
CustomJS PDF Toolkit Self-hosted n8n users who accept a community node and external API Requires self-hosted n8n and a CustomJS API key

PDF.co exposes a DoNotWaitFullLoad option: false waits for full page load, while true waits only for minimal loading. Treat this as a provider-specific API option, not a universal solution to blank PDFs.

6. Troubleshooting checklist

Symptom Likely cause Fix
400 response from Gotenberg Missing upload or wrong field name Send multipart form data with a file field named exactly index.html.
Blank PDF JavaScript or external data was not ready Add an appropriate wait/render control and confirm the page contains data before upload.
Missing styles or images Assets are unreachable from the container Use reachable URLs, inline critical assets, or fix container DNS and network access.
Fonts fall back Font URL or file is inaccessible Make the font reachable and check its response from the Gotenberg environment.
HTTP node returns text instead of a PDF Response format is not binary Set the response to File/binary and map the resulting binary property downstream.
n8n cannot connect Wrong hostname, port, or Docker network Confirm that n8n can resolve gotenberg and reach port 3000.
Only some records fail Malformed data creates invalid HTML Log the generated HTML, escape inserted values, and validate required fields before conversion.
Workflow times out Large pages, slow assets, or expensive scripts Reduce asset size, avoid unnecessary scripts, raise appropriate workflow timeouts, and process batches deliberately.

During diagnosis, save the generated HTML, the HTTP status, response body, execution ID, and the input URL or record identifier. Deterministic filenames make it easier to match a PDF to a request.

7. Performance, reliability, and cost

Performance

  • Inline only critical CSS; large images and fonts increase transfer and rendering time.
  • Reuse a warm Gotenberg service instead of starting a container for every item.
  • Process large batches with controlled concurrency so Chromium does not exhaust CPU or memory.
  • Use a condition-based wait for dynamic content where possible instead of a long fixed delay.

Reliability

  • Pin and update the Gotenberg image deliberately; the n8n template’s gotenberg/gotenberg:8 is an example, not a permanent version promise.
  • Make retries idempotent by deriving output names from a stable record ID.
  • Record request and execution identifiers, HTTP status, and output metadata.
  • Test pages with slow assets, missing optional fields, long text, and empty datasets.

Cost

Self-hosting shifts cost to the compute, storage, networking, and maintenance of n8n and Gotenberg. Hosted APIs shift those operations to the vendor but add provider limits and usage charges. The research sources do not provide comparable throughput, latency, failure-rate, or pricing figures, so measure your own workload before choosing a route.

8. Or skip the browser setup

ScreenshotNeo provides a website screenshot and PDF API. One GET request returns a PDF or image, so you do not have to run Chromium beside n8n. Cookie and consent banners are accepted and removed before capture, along with more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. An MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.

See the ScreenshotNeo API documentation for the available PDF options.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -d format=pdf \
  -o page.pdf
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com", "format": "pdf"},
    timeout=90,
)
r.raise_for_status()
open("page.pdf", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com',
  format: 'pdf'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('page.pdf', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo has 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

9. FAQ

Can n8n generate a PDF without Gotenberg?

Yes. You can use a hosted HTML-to-PDF integration or API, or a community node such as the CustomJS route. Gotenberg is the self-hosted Chromium option described in this guide.

Why must the file be called index.html?

That is the required multipart file name for Gotenberg’s HTML conversion endpoint. Other names can result in a rejected or unprocessed request.

Should I convert HTML to base64 in the HTTP Request node?

No. n8n should send the HTML as a binary multipart file. Base64 is only the internal representation used when constructing n8n binary data.

How do I make page breaks predictable?

Use print CSS such as @page, explicit margins, and page-break rules, then test with long and short content. Chromium layout still depends on the final rendered dimensions.

Can I convert a URL instead of an HTML string?

Gotenberg’s HTML form endpoint expects an uploaded index.html. For URL-to-PDF, use an integration or hosted service that explicitly supports URL input, or fetch and assemble the HTML before the Gotenberg step.