arrow_back Back to blog
Tutorials March 6, 2026 8 min read

URL to PDF API: The Complete Developer Guide

Converting a URL or HTML to a PDF sounds simple. In practice, it’s one of those problems that gets complicated fast — page breaks, margins, header/footer templates, background colors, print stylesheets, and the endless quirks of browser rendering engines.

The traditional approach involves running a headless browser (Puppeteer or Playwright), managing Chrome processes, and wrestling with the page.pdf() API. It works, but it’s a lot of infrastructure for what should be a straightforward conversion.

The len.sh /v1/pdf endpoint turns any URL or HTML into a PDF with a single HTTP request. This guide covers every parameter, real-world use cases, and code examples in multiple languages.


Quick Start

The simplest possible PDF generation — convert a URL to A4:

curl "https://api.len.sh/v1/pdf?url=https://example.com&access_key=YOUR_API_KEY" \
  --output document.pdf

That’s a working PDF. The default settings (A4 paper, 0.4in margins, portrait orientation, backgrounds included) cover most use cases.


All PDF Parameters

ParameterTypeDefaultDescription
urlstringrequired*URL to render as PDF
htmlstringrequired*Raw HTML to render (use POST)
paper_sizestringA4Page size: A4, Letter, Legal, Tabloid, Ledger
landscapebooleanfalseLandscape orientation
scalenumber1.0Scale factor (0.1-2.0)
margin_topstring0.4inTop margin (CSS unit: in, cm, mm, px)
margin_rightstring0.4inRight margin
margin_bottomstring0.4inBottom margin
margin_leftstring0.4inLeft margin
print_backgroundbooleantrueInclude background colors/images
page_rangesstringPages to include, e.g. 1-3, 5
header_templatestringHTML template for page headers
footer_templatestringHTML template for page footers
wait_untilstringloadWhen to consider the page loaded
timeoutinteger15000Max wait time in ms (1-30000)
delayinteger0Wait N ms before rendering (0-10000)
user_agentstringCustom user agent string
block_adsbooleanfalseHide ad elements
block_cookie_bannersbooleanfalseHide cookie banners
jsstringCustom JavaScript to inject (max 10KB)
cssstringCustom CSS to inject (max 10KB)
cache_ttlinteger86400Cache duration in seconds
response_typestringpdfpdf (raw bytes) or json (metadata)

*One of url or html is required.


Paper Sizes

The paper_size parameter supports standard page sizes:

SizeDimensionsCommon Use
A4210mm x 297mmInternational standard, business documents
Letter8.5in x 11inUS standard, business documents
Legal8.5in x 14inUS legal documents, contracts
Tabloid11in x 17inPosters, large-format printouts
Ledger17in x 11inSpreadsheets, landscape charts
# US Letter size
curl "https://api.len.sh/v1/pdf?url=https://example.com&paper_size=Letter&access_key=YOUR_API_KEY" \
  --output letter.pdf

# Legal size for contracts
curl "https://api.len.sh/v1/pdf?url=https://example.com/contract&paper_size=Legal&access_key=YOUR_API_KEY" \
  --output contract.pdf

Margins

Margins accept any CSS unit: in, cm, mm, or px. You can set each side independently:

# 1-inch margins on all sides
curl "https://api.len.sh/v1/pdf?url=https://example.com&margin_top=1in&margin_right=1in&margin_bottom=1in&margin_left=1in&access_key=YOUR_API_KEY" \
  --output with-margins.pdf

# Narrow margins for maximum content area
curl "https://api.len.sh/v1/pdf?url=https://example.com&margin_top=0.25in&margin_right=0.25in&margin_bottom=0.25in&margin_left=0.25in&access_key=YOUR_API_KEY" \
  --output narrow.pdf

# No margins (full bleed)
curl "https://api.len.sh/v1/pdf?url=https://example.com&margin_top=0&margin_right=0&margin_bottom=0&margin_left=0&access_key=YOUR_API_KEY" \
  --output bleed.pdf

Headers and Footers

The header_template and footer_template parameters accept HTML strings that are rendered on every page. Chrome’s PDF engine supports special CSS classes for dynamic content:

ClassContent
dateCurrent date
titleDocument title
urlDocument URL
pageNumberCurrent page number
totalPagesTotal page count
curl -X POST https://api.len.sh/v1/pdf \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/report",
    "paper_size": "A4",
    "margin_top": "1in",
    "margin_bottom": "1in",
    "header_template": "<div style=\"font-size:10px;text-align:center;width:100%\">Quarterly Report - Confidential</div>",
    "footer_template": "<div style=\"font-size:10px;text-align:center;width:100%\">Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></div>"
  }' \
  --output report.pdf

Note: header and footer templates need sufficient margin space. If your margins are too small, the header/footer content may overlap with the page content.


Page Ranges

Extract specific pages from a multi-page document:

# First 3 pages only
curl "https://api.len.sh/v1/pdf?url=https://example.com/long-doc&page_ranges=1-3&access_key=YOUR_API_KEY" \
  --output first-three.pdf

# Pages 1, 3, and 5-7
curl "https://api.len.sh/v1/pdf?url=https://example.com/long-doc&page_ranges=1,3,5-7&access_key=YOUR_API_KEY" \
  --output selected.pdf

This is useful for generating executive summaries from full reports, or extracting specific sections from long documents.


Real-World Use Cases

1. Invoice Generation

Invoices are the most common PDF use case. If you have an invoice template that renders as a web page, converting it to PDF is one API call:

async function generateInvoice(invoiceId) {
  const params = new URLSearchParams({
    url: `https://yourapp.com/invoices/${invoiceId}`,
    paper_size: "Letter",
    margin_top: "0.75in",
    margin_right: "0.75in",
    margin_bottom: "0.75in",
    margin_left: "0.75in",
    print_background: "true",
    access_key: process.env.LENSH_API_KEY,
  });

  const response = await fetch(`https://api.len.sh/v1/pdf?${params}`);
  return Buffer.from(await response.arrayBuffer());
}

// Usage
const pdfBuffer = await generateInvoice("INV-2026-001");
// Save to disk, upload to S3, attach to email, etc.

2. Report Export

Dashboards and reports often need a “Download PDF” button. Instead of building a separate PDF rendering pipeline, screenshot the existing dashboard:

import requests
import os

def export_report(report_url: str, output_path: str):
    response = requests.get(
        "https://api.len.sh/v1/pdf",
        params={
            "url": report_url,
            "paper_size": "A4",
            "landscape": "true",  # Better for wide dashboards
            "margin_top": "0.5in",
            "margin_right": "0.5in",
            "margin_bottom": "0.5in",
            "margin_left": "0.5in",
            "print_background": "true",
            "delay": "2000",  # Wait for charts to render
            "wait_until": "networkidle2",
            "access_key": os.environ["LENSH_API_KEY"],
        },
        stream=True,
    )
    response.raise_for_status()

    with open(output_path, "wb") as f:
        for chunk in response.iter_content(chunk_size=8192):
            f.write(chunk)

export_report(
    "https://yourapp.com/reports/q4-2026",
    "q4-report.pdf"
)

The delay=2000 and wait_until=networkidle2 parameters ensure charts and async data have time to load before the PDF is captured.

3. Documentation Export

Convert documentation pages to PDF for offline distribution:

# Export a single doc page
curl "https://api.len.sh/v1/pdf?url=https://docs.yourapp.com/getting-started&paper_size=A4&print_background=true&block_ads=true&block_cookie_banners=true&access_key=YOUR_API_KEY" \
  --output getting-started.pdf

The block_ads and block_cookie_banners parameters clean up the page before rendering, removing distracting elements that shouldn’t appear in documentation PDFs.

For legal documents, you typically want precise margins and page numbering:

curl -X POST https://api.len.sh/v1/pdf \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/contracts/2026-NDA",
    "paper_size": "Letter",
    "margin_top": "1in",
    "margin_right": "1in",
    "margin_bottom": "1in",
    "margin_left": "1in",
    "print_background": "false",
    "footer_template": "<div style=\"font-size:9px;text-align:center;width:100%\">Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span> | Confidential</div>"
  }' \
  --output nda.pdf

5. Generating PDFs from Raw HTML

You don’t need a hosted URL — you can render HTML directly via POST:

async function htmlToPdf(htmlContent) {
  const response = await fetch("https://api.len.sh/v1/pdf", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.LENSH_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      html: htmlContent,
      paper_size: "A4",
      margin_top: "0.75in",
      margin_right: "0.75in",
      margin_bottom: "0.75in",
      margin_left: "0.75in",
      print_background: true,
    }),
  });

  return Buffer.from(await response.arrayBuffer());
}

// Usage
const html = `
  <html>
    <body style="font-family: system-ui; padding: 40px;">
      <h1>Invoice #INV-001</h1>
      <table style="width: 100%; border-collapse: collapse;">
        <tr style="border-bottom: 1px solid #ddd;">
          <th style="text-align: left; padding: 8px;">Item</th>
          <th style="text-align: right; padding: 8px;">Amount</th>
        </tr>
        <tr style="border-bottom: 1px solid #ddd;">
          <td style="padding: 8px;">API Usage - Pro Plan</td>
          <td style="text-align: right; padding: 8px;">€19.00</td>
        </tr>
        <tr>
          <td style="padding: 8px; font-weight: bold;">Total</td>
          <td style="text-align: right; padding: 8px; font-weight: bold;">€19.00</td>
        </tr>
      </table>
    </body>
  </html>
`;

const pdf = await htmlToPdf(html);

This is useful for dynamically generated content where you don’t have a hosted URL — email receipts, one-off documents, or content assembled from database records.


Injecting Custom CSS

Sometimes the web version of a page looks great but the PDF version needs adjustments — hiding navigation, adjusting font sizes, or changing the layout for print. Use the css parameter to inject styles:

curl -X POST https://api.len.sh/v1/pdf \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/dashboard",
    "paper_size": "A4",
    "landscape": true,
    "css": "nav, footer, .sidebar { display: none !important; } .main-content { width: 100% !important; } body { font-size: 12px; }"
  }' \
  --output dashboard.pdf

This hides the navigation, footer, and sidebar, makes the main content full-width, and reduces the font size — all without modifying the original page.


Injecting Custom JavaScript

For pages that need interaction before rendering (closing modals, expanding sections, clicking “show all”):

curl -X POST https://api.len.sh/v1/pdf \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/report",
    "paper_size": "A4",
    "delay": 1000,
    "js": "document.querySelectorAll(\"details\").forEach(d => d.open = true); document.querySelector(\".cookie-banner\")?.remove();"
  }' \
  --output report.pdf

The JavaScript runs after the page loads but before the PDF is captured. The delay parameter gives the DOM time to settle after the script executes.


Scaling and Performance

Caching

PDFs are cached on Cloudflare’s edge network. The same URL with the same parameters returns a cached result in ~50-100ms. The default cache TTL is 24 hours.

# First request: generates the PDF (~2-5 seconds)
curl "https://api.len.sh/v1/pdf?url=https://example.com&access_key=YOUR_API_KEY" \
  --output doc.pdf

# Second request: served from cache (~50-100ms)
curl "https://api.len.sh/v1/pdf?url=https://example.com&access_key=YOUR_API_KEY" \
  --output doc.pdf

For dynamic content that changes frequently, set cache_ttl=0 to bypass the cache.

Parallel Generation

If you need to generate multiple PDFs, run requests in parallel:

const urls = [
  "https://yourapp.com/invoices/001",
  "https://yourapp.com/invoices/002",
  "https://yourapp.com/invoices/003",
];

const pdfs = await Promise.all(
  urls.map(async (url) => {
    const params = new URLSearchParams({
      url,
      paper_size: "Letter",
      print_background: "true",
      access_key: process.env.LENSH_API_KEY,
    });

    const response = await fetch(`https://api.len.sh/v1/pdf?${params}`);
    return {
      url,
      buffer: Buffer.from(await response.arrayBuffer()),
    };
  })
);

Rate limits vary by plan, but even the free tier supports reasonable concurrency for batch operations.


If the target page has a @media print stylesheet, the PDF will use it. This is the right way to handle print-specific styling for pages you control:

/* In your site's CSS */
@media print {
  nav, footer, .sidebar, .cookie-banner {
    display: none;
  }
  .main-content {
    width: 100%;
    margin: 0;
    padding: 0;
  }
  a[href]::after {
    content: " (" attr(href) ")";
    font-size: 0.8em;
    color: #666;
  }
}

The len.sh PDF engine respects these styles automatically. No additional configuration needed.


Getting Started

  1. Sign up for a free len.sh account
  2. Try the simplest request:
curl "https://api.len.sh/v1/pdf?url=https://example.com&access_key=YOUR_API_KEY" \
  --output test.pdf
  1. Customize with paper size, margins, and background settings
  2. Add headers/footers for professional documents
  3. Integrate into your application with the code examples above

The free tier includes 100 requests per month — enough to build and validate your PDF pipeline. All PDF parameters are available on every plan, with no feature gating.

Try len.sh for free

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