arrow_back Back to blog
Tutorials April 12, 2026 6 min read

HTML to PDF API: Convert Templates to Professional PDFs

Generating PDFs from HTML is one of the most common tasks in web development. Invoices, reports, certificates, contracts, shipping labels — any document that needs to look the same everywhere ends up as a PDF.

The existing URL to PDF guide covers converting live URLs to PDF. This guide focuses on the other half: converting raw HTML templates to PDF using POST requests. No hosted page required.


Quick Start

Send HTML in a POST request to get a PDF back:

curl -X POST "https://api.len.sh/v1/pdf?access_key=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html": "<h1>Hello, PDF</h1><p>Generated from raw HTML.</p>"}' \
  --output hello.pdf

That produces a single-page A4 PDF. The HTML is rendered with a full Chromium engine, so CSS works exactly as you’d expect — flexbox, grid, custom fonts, the lot.


Why HTML to PDF (Instead of a URL)?

Using raw HTML instead of a hosted URL gives you:

  1. No deployment needed — generate PDFs from templates without hosting a page
  2. Dynamic data — inject customer names, line items, totals, dates directly into the HTML
  3. Version control — store your PDF templates alongside your code
  4. Privacy — sensitive data (invoices, contracts) never hits a public URL
  5. Speed — no DNS lookup, no network fetch, just render and return

Invoice Example

Here’s a realistic invoice template:

curl -X POST "https://api.len.sh/v1/pdf?access_key=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<!DOCTYPE html><html><head><style>body{font-family:system-ui,sans-serif;margin:40px;color:#1a1a1a}h1{color:#4850e5;margin-bottom:4px}.invoice-meta{color:#666;margin-bottom:32px}table{width:100%;border-collapse:collapse;margin:24px 0}th{text-align:left;padding:12px;background:#f8f9fa;border-bottom:2px solid #e2e8f0;font-size:14px}td{padding:12px;border-bottom:1px solid #e2e8f0;font-size:14px}.total-row td{font-weight:bold;border-top:2px solid #1a1a1a;border-bottom:none}.right{text-align:right}</style></head><body><h1>Invoice #2026-0042</h1><p class=\"invoice-meta\">April 12, 2026</p><p><strong>Bill to:</strong> Acme Corp<br>123 Business Ave, Copenhagen</p><table><thead><tr><th>Description</th><th class=\"right\">Qty</th><th class=\"right\">Unit Price</th><th class=\"right\">Amount</th></tr></thead><tbody><tr><td>API Integration (March)</td><td class=\"right\">40 hrs</td><td class=\"right\">EUR 150</td><td class=\"right\">EUR 6,000</td></tr><tr><td>Infrastructure hosting</td><td class=\"right\">1</td><td class=\"right\">EUR 200</td><td class=\"right\">EUR 200</td></tr><tr class=\"total-row\"><td colspan=\"3\">Total</td><td class=\"right\">EUR 6,200</td></tr></tbody></table><p style=\"margin-top:32px;font-size:13px;color:#666\">Payment due within 14 days. Bank: Danske Bank, IBAN: DK12 3456 7890 1234</p></body></html>",
    "paper_size": "A4",
    "margin_top": "0.75in",
    "margin_bottom": "0.75in"
  }' --output invoice.pdf

In practice, you’d build the HTML string in your application code with real data.


Building Templates in Code

JavaScript / TypeScript

interface InvoiceItem {
  description: string;
  quantity: string;
  unitPrice: number;
  amount: number;
}

function buildInvoiceHTML(
  invoiceNumber: string,
  customer: string,
  items: InvoiceItem[],
  total: number
): string {
  const rows = items
    .map(
      (item) => `
    <tr>
      <td>${item.description}</td>
      <td class="right">${item.quantity}</td>
      <td class="right">EUR ${item.unitPrice.toFixed(2)}</td>
      <td class="right">EUR ${item.amount.toFixed(2)}</td>
    </tr>`
    )
    .join("");

  return `<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: system-ui, sans-serif; margin: 40px; color: #1a1a1a; }
    h1 { color: #4850e5; }
    table { width: 100%; border-collapse: collapse; margin: 24px 0; }
    th { text-align: left; padding: 12px; background: #f8f9fa; border-bottom: 2px solid #e2e8f0; }
    td { padding: 12px; border-bottom: 1px solid #e2e8f0; }
    .right { text-align: right; }
    .total-row td { font-weight: bold; border-top: 2px solid #1a1a1a; }
  </style>
</head>
<body>
  <h1>Invoice ${invoiceNumber}</h1>
  <p><strong>Bill to:</strong> ${customer}</p>
  <table>
    <thead><tr><th>Description</th><th class="right">Qty</th><th class="right">Unit Price</th><th class="right">Amount</th></tr></thead>
    <tbody>
      ${rows}
      <tr class="total-row">
        <td colspan="3">Total</td>
        <td class="right">EUR ${total.toFixed(2)}</td>
      </tr>
    </tbody>
  </table>
</body>
</html>`;
}

// Generate the PDF
const html = buildInvoiceHTML("2026-0042", "Acme Corp", items, 6200);

const response = await fetch(
  "https://api.len.sh/v1/pdf?access_key=YOUR_API_KEY",
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ html }),
  }
);

const pdf = await response.arrayBuffer();

Python

import requests

def generate_pdf(html: str, filename: str):
    response = requests.post(
        "https://api.len.sh/v1/pdf",
        params={"access_key": "YOUR_API_KEY"},
        json={
            "html": html,
            "paper_size": "A4",
            "margin_top": "0.75in",
            "margin_bottom": "0.75in",
        },
    )
    response.raise_for_status()
    with open(filename, "wb") as f:
        f.write(response.content)

Headers and Footers

Add consistent headers and footers across every page using HTML templates. These templates support special CSS classes that Chromium replaces with dynamic values:

ClassReplaced with
dateCurrent date
titleDocument title
urlDocument URL
pageNumberCurrent page number
totalPagesTotal page count
curl -X POST "https://api.len.sh/v1/pdf?access_key=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<h1>Quarterly Report</h1><p>Content spanning multiple pages...</p>",
    "header_template": "<div style=\"font-size:10px;width:100%;text-align:center;color:#999\">Confidential - Acme Corp</div>",
    "footer_template": "<div style=\"font-size:10px;width:100%;padding:0 40px;display:flex;justify-content:space-between;color:#999\"><span>Q1 2026 Report</span><span>Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></span></div>",
    "margin_top": "1in",
    "margin_bottom": "0.75in"
  }' --output report.pdf

Note: headers and footers render inside the margin area. Make sure your margins are large enough to accommodate them.


Paper Sizes and Orientation

SizeDimensions
A4 (default)210 x 297 mm
Letter8.5 x 11 in
Legal8.5 x 14 in
Tabloid11 x 17 in
Ledger17 x 11 in

Switch to landscape for wide tables or dashboards:

{
  "html": "<table>...</table>",
  "paper_size": "A4",
  "landscape": true
}

CSS Print Styles

The Chromium engine respects @media print rules and print-specific CSS properties. Use these to control page breaks:

<style>
  /* Force a page break before each section */
  .section { page-break-before: always; }

  /* Prevent breaking inside a table row */
  tr { page-break-inside: avoid; }

  /* Hide elements that shouldn't appear in PDF */
  @media print {
    .no-print { display: none; }
  }
</style>

Background colors and images are included by default (print_background: true). Set it to false if you want a print-optimized output without backgrounds.


Common Document Types

Report with cover page

<div style="height: 100vh; display: flex; align-items: center; justify-content: center; flex-direction: column;">
  <h1 style="font-size: 48px;">Q1 2026 Report</h1>
  <p style="font-size: 24px; color: #666;">Acme Corporation</p>
</div>

<div class="section" style="page-break-before: always;">
  <h2>Executive Summary</h2>
  <p>...</p>
</div>

Certificate

<div style="border: 8px double #4850e5; padding: 60px; text-align: center; height: 100vh; display: flex; flex-direction: column; justify-content: center;">
  <p style="font-size: 14px; text-transform: uppercase; letter-spacing: 4px; color: #666;">Certificate of Completion</p>
  <h1 style="font-size: 36px; margin: 20px 0;">Jane Smith</h1>
  <p style="font-size: 18px; color: #444;">Has completed the Advanced API Integration course</p>
  <p style="margin-top: 40px; color: #666;">April 12, 2026</p>
</div>

Shipping label

Use Letter paper size with custom margins:

{
  "html": "<div style='font-family: monospace; font-size: 14px;'>...</div>",
  "paper_size": "Letter",
  "margin_top": "0.25in",
  "margin_bottom": "0.25in",
  "margin_left": "0.25in",
  "margin_right": "0.25in"
}

Scaling

The scale parameter adjusts the zoom level of the rendered page. Values between 0.1 and 2.0:

{
  "html": "...",
  "scale": 0.8
}

A scale of 0.8 shrinks content to 80%, fitting more on each page. Useful for dense tables or dashboards that don’t quite fit at 100%.


Blocking Unwanted Elements

If your HTML loads external resources that include ads, cookie banners, or popups, use the built-in blockers:

{
  "url": "https://your-app.com/report",
  "block_ads": true,
  "block_cookie_banners": true,
  "block_popups": true
}

For raw HTML templates, this is rarely needed since you control the markup.


Performance Tips

  1. Inline your CSS — avoid external stylesheet fetches. Put styles in a <style> tag.
  2. Inline images as base64<img src="data:image/png;base64,..."> avoids network requests.
  3. Use wait_until: domcontentloaded — if your HTML is self-contained with no external resources, skip waiting for the full load event.
  4. Set a reasonable timeout — the default 15 seconds is generous. For simple templates, 5 seconds is plenty.
  5. Cache repeated PDFs — if the same document is generated often, set a cache_ttl to serve from Cloudflare’s edge.

Pricing

PDF generation uses the same quota as screenshots. Each PDF counts as one capture. The Free tier includes 100/month. Pro (25,000/month) starts at EUR 19/month. All plans include full API access with no feature gating.

Try len.sh for free

Start capturing screenshots with a simple API call. No credit card required.