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:
- No deployment needed — generate PDFs from templates without hosting a page
- Dynamic data — inject customer names, line items, totals, dates directly into the HTML
- Version control — store your PDF templates alongside your code
- Privacy — sensitive data (invoices, contracts) never hits a public URL
- 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:
| Class | Replaced with |
|---|---|
date | Current date |
title | Document title |
url | Document URL |
pageNumber | Current page number |
totalPages | Total 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
| Size | Dimensions |
|---|---|
A4 (default) | 210 x 297 mm |
Letter | 8.5 x 11 in |
Legal | 8.5 x 14 in |
Tabloid | 11 x 17 in |
Ledger | 17 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
- Inline your CSS — avoid external stylesheet fetches. Put styles in a
<style>tag. - Inline images as base64 —
<img src="data:image/png;base64,...">avoids network requests. - Use
wait_until: domcontentloaded— if your HTML is self-contained with no external resources, skip waiting for the fullloadevent. - Set a reasonable timeout — the default 15 seconds is generous. For simple templates, 5 seconds is plenty.
- Cache repeated PDFs — if the same document is generated often, set a
cache_ttlto 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.