DEV Community

Cover image for Rendering Pixel-Accurate PDFs From Raw HTML, No Headless Browser Required
PDF4me
PDF4me

Posted on

Rendering Pixel-Accurate PDFs From Raw HTML, No Headless Browser Required

If you've ever generated a PDF from HTML, you've probably met the headless browser. Puppeteer, Playwright, wkhtmltopdf, some flavor of Chromium running invisibly on a server somewhere, waiting to render a page and hand you back a PDF. It works. It also means you're now responsible for keeping a browser binary alive in production: patching it, sizing memory for it, restarting it when it silently hangs on a font that never loaded. That's a lot of infrastructure for what is, conceptually, a single conversion step.

PDF4me's Convert HTML to PDF endpoint skips that entirely. You send HTML, base64-encoded, to a single REST endpoint. PDF4me renders it server-side and hands back a finished PDF. No browser to install, no headless process to babysit. PDF4me's own Make integration page puts it plainly: the module "renders HTML content or a public web URL into a finished PDF document without a headless browser or extra rendering tools." That's the whole pitch, and it's a useful one if you've spent an afternoon debugging why a headless Chrome instance ran out of memory on a Friday.

What the endpoint actually takes

The REST call is POST /api/v2/ConvertHtmlToPdf. At minimum you're sending docContent (your HTML, base64-encoded), docName for the output file, and indexFilePath, which is where things get more useful than they first look. You're not limited to a single HTML file: PDF4me accepts a ZIP archive containing HTML plus its referenced assets (CSS files, images, fonts), and indexFilePath tells PDF4me which file inside that ZIP is the entry point. If your invoice template pulls in a separate stylesheet and a logo image, you don't need to inline everything into one file. Zip it, point PDF4me at the index page, done.

Here's a live-verified Python sample, adapted from PDF4me's own pdf4me-api-samples repo (MIT licensed):

import requests
import base64

api_key = "YOUR_API_KEY"  # from https://dev.pdf4me.com/dashboard/#/api-keys/
url = "https://api.pdf4me.com/api/v2/ConvertHtmlToPdf"

with open("invoice.html", "rb") as f:
    html_base64 = base64.b64encode(f.read()).decode("utf-8")

payload = {
    "docContent": html_base64,
    "docName": "output.pdf",
    "indexFilePath": "invoice.html",
    "layout": "Portrait",
    "format": "A4",
    "scale": 0.8,
    "topMargin": "40px",
    "bottomMargin": "40px",
    "leftMargin": "40px",
    "rightMargin": "40px",
    "printBackground": True,
    "displayHeaderFooter": True,
    "async": True
}

headers = {
    "Authorization": f"Basic {api_key}",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

if response.status_code == 200:
    with open("output.pdf", "wb") as f:
        f.write(response.content)
elif response.status_code == 202:
    # Async: poll the Location header URL until it returns 200
    location_url = response.headers.get("Location")
Enter fullscreen mode Exit fullscreen mode

Two things worth flagging here since I hit both while cross-checking the docs against the actual sample code. First, the real request payload sends a lowercase "async": true, while the API Tester for this same endpoint documents it as IsAsync. Second, the Convert HTML to PDF REST page's own text says scale runs "between 0 and 0.8," but the working sample code's own comment, the API Tester, and the sibling Convert URL to PDF page all agree on a 0.1 to 2.0 range where 1.0 is original size. If precise scaling matters, test the real boundary in the API Tester rather than trusting either doc page blindly.

Where your page dimensions actually come from

Here's the part that trips people up coming from a headless-browser mental model: layout only controls Portrait versus Landscape. It does not control page size, margins, or fonts by itself. Those come from the CSS inside your HTML. PDF4me's Make integration guide is direct about this: "Page dimensions and margins in the output PDF come entirely from the CSS in your HTML... the Layout dropdown only sets Portrait vs Landscape orientation." If you want a true A4 page with a 20mm margin, you write it into a style block with an @page rule, not into a PDF4me parameter.

That same guide surfaces a gotcha worth repeating because it will bite you exactly once and then never again: inline styles and embedded <style> blocks render reliably, but external stylesheets loaded via a <link> tag only work if PDF4me can reach that URL publicly at render time. If your CSS lives behind a login, a VPN, or a staging environment PDF4me's servers can't hit, your styling silently disappears from the output. No error, just a PDF that doesn't look like your preview. Inline it or embed it if you're not certain the link is public.

Same endpoint, four different shapes

The REST call is the mechanism. Each integration platform wraps it differently, and the differences matter for which one fits your stack.

Make gives you a two-field module: Document / public file URL accepts either binary HTML content from an earlier module or, notably, a live public web address directly. If the page you want already exists and is publicly reachable, you can skip building HTML entirely and just point Make at the URL.

Zapier keeps the same idea even simpler: map an HTML file, choose Portrait or Landscape, done. It's the thinnest wrapper of the four.

Power Automate mirrors the REST parameter set closely, including ZIP support with an index file path, which fits teams already pulling multi-file HTML templates out of SharePoint or OneDrive.

n8n is the most flexible of the four. Its node accepts four separate input types: Binary Data, Base64 String, raw HTML Code typed directly into the node, or a public URL. That HTML Code option is genuinely useful for quick internal tooling, you can write throwaway HTML straight into the workflow without a prior node generating it.

If you'd rather convert an already-live page directly instead of assembling HTML yourself, that's a different endpoint: Convert URL to PDF takes a web address plus optional authentication (Basic, OAuth, or API key) and handles the rest.

Try it before you write a line of code

Every parameter above is testable directly in the API Tester without writing a request by hand. Upload a file, fill the fields, hit send, and you get back either a 200 with the PDF, or a 202 with a Location header to poll if you set async processing on. That async path matters if your HTML is large or you're batching, since polling means you're not holding a connection open waiting on a synchronous render.

Why this is worth doing without a browser at all

Converting HTML to PDF is one of the more common document automation asks there is: invoices built from a template, weekly reports assembled from a database query, an email body archived as a compliance record. What changes when you route it through a rendering API instead of a headless browser is what you're no longer responsible for. No browser process to keep patched. No memory leak from a page that never finished loading. No debugging why a font rendered differently in your CI container than on your laptop. You send HTML and parameters, you get back a PDF.

That's the trade worth understanding before you reach for Puppeteer out of habit: a REST call has a fixed, documented parameter surface. Does a headless browser really need to be your default anymore?

Website: pdf4me.com
Documentation: docs.pdf4me.com
Developer portal: dev.pdf4me.com

Top comments (0)