GUIDE · 2026-06-24 · 7 min read

How to convert HTML to PDF with an API (5 languages)

Real working code in JavaScript, Python, PHP, Ruby and Go to turn an HTML string — or a live URL — into a PDF over one REST endpoint. No headless Chrome to babysit.

Converting HTML to PDF in code is one of those tasks that looks trivial until you try to self-host it. Below is the one-call version in five languages using the SnapPDF API, then the honest trade-offs.

curl -X POST https://snappdf.au/api/v1/html-to-pdf \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>Invoice #1024</h1>"}' \
  --output invoice.pdf

Every example below hits that same endpoint: POST https://snappdf.au/api/v1/html-to-pdf. Authenticate with a Bearer key — sk_live_ for production, sk_test_ for sandboxed tests. One endpoint, two input shapes (html or url), and a PDF in the response.

Why use an API instead of headless Chrome?

The DIY route is Puppeteer or Playwright driving headless Chrome. It works on your laptop and then bites you in production: Chrome is a ~150 MB dependency, it leaks memory under load, and serverless platforms cap you well under the RAM a Chromium process wants. An API moves that one moving part off your box — you send JSON, you get bytes back. The trade-off is honest: you pay per call (HTML-to-PDF is 5 credits on SnapPDF) instead of paying in ops time.

JavaScript / TypeScript

Install the SDK with one line: npm i snappdf. The SDK is a thin typed wrapper over the REST call.

import { SnapPDF } from "snappdf";
import { writeFile } from "node:fs/promises";

const snap = new SnapPDF({ apiKey: process.env.SNAPPDF_KEY });

const pdf = await snap.htmlToPdf({
  html: "<h1>Invoice #1024</h1><p>Thanks for your business.</p>",
  format: "A4",
  printBackground: true,
});

await writeFile("invoice.pdf", pdf);

That is the whole integration — zero extra runtime dependencies beyond the SDK, because the rendering happens server-side.

Python

One install: pip install snappdf. Same shape, snake_case method.

import os
from snappdf import SnapPDF

snap = SnapPDF(api_key=os.environ["SNAPPDF_KEY"])

pdf = snap.html_to_pdf(
    html="<h1>Invoice #1024</h1><p>Thanks for your business.</p>",
    format="A4",
    print_background=True,
)

with open("invoice.pdf", "wb") as f:
    f.write(pdf)

This is a single blocking call; wrap it in a thread or use the async client if you are inside an event loop. The response is 1 PDF as raw bytes.

PHP

Install with Composer: composer require snappdf/snappdf. Useful for rendering Blade/Twig output straight to PDF.

<?php
require 'vendor/autoload.php';

use SnapPDF\Client;

$snap = new Client(getenv('SNAPPDF_KEY'));

$pdf = $snap->htmlToPdf([
    'html'   => '<h1>Invoice #1024</h1><p>Thanks for your business.</p>',
    'format' => 'A4',
    'printBackground' => true,
]);

file_put_contents('invoice.pdf', $pdf);

Render your template engine's HTML, pass the string in, write the bytes out — 3 lines of real work.

Ruby

Install the gem: gem install snappdf. Drops cleanly into a Rails controller or a Sidekiq job.

require "snappdf"

snap = SnapPDF::Client.new(api_key: ENV["SNAPPDF_KEY"])

pdf = snap.html_to_pdf(
  html: "<h1>Invoice #1024</h1><p>Thanks for your business.</p>",
  format: "A4",
  print_background: true
)

File.binwrite("invoice.pdf", pdf)

In a Rails controller you would send_data pdf, type: "application/pdf" instead of writing to disk — still 1 call.

Go

Install: go get github.com/seaqae/snappdf-go. The Go client returns ([]byte, error) like every other Go API.

package main

import (
	"log"
	"os"

	"github.com/seaqae/snappdf-go"
)

func main() {
	client := snappdf.New(os.Getenv("SNAPPDF_KEY"))

	pdf, err := client.HTMLToPDF(snappdf.HTMLToPDFRequest{
		HTML:            "<h1>Invoice #1024</h1>",
		Format:          "A4",
		PrintBackground: true,
	})
	if err != nil {
		log.Fatal(err)
	}

	if err := os.WriteFile("invoice.pdf", pdf, 0644); err != nil {
		log.Fatal(err)
	}
}

Idiomatic error handling, no cgo, no Chrome — the binary stays small across all 5 languages because none of them bundle a browser.

Rendering a live URL instead of an HTML string

If the document already lives at a URL, skip the HTML and send the address. It is the same endpoint and the same 5-credit cost.

curl -X POST https://snappdf.au/api/v1/html-to-pdf \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/report","format":"A4"}' \
  --output report.pdf

SnapPDF loads the page server-side with fonts and CSS intact. For pages that render content with JavaScript, add "waitFor": 1500 (milliseconds) or "waitFor": "#chart-ready" (a CSS selector) so the snapshot waits for the content to exist.

Common options

All options ride in the same JSON body. The 6 you will reach for most:

  • format — page size: A4 (default), Letter, Legal, A3, A5, Tabloid.
  • landscape — boolean, default false.
  • margin{ top, right, bottom, left }, each accepts px/mm/cm/in.
  • printBackground — render CSS backgrounds, default true.
  • waitFor — milliseconds or a CSS selector for JS-rendered pages.
  • scale — 0.1–2.0 zoom, default 1.

What it costs

HTML-to-PDF and URL-to-PDF are 5 credits per call. The free tier is 100 credits a month that never expire, which covers 20 conversions monthly at zero cost — enough to build and test a real integration before you pay anything. Paid plans start at $4/month, and top-up packs (1,000 credits for $10) never expire.

Next

TRY SNAPPDF

Free for 100 credits a month.

Try the core tools on the Free plan, then move to Personal, Pro or Business when PDF work becomes regular.

Open tools