HTML to PDF API: Paper, Margins, Page Numbers

Turn a URL or your HTML into a PDF with real Chrome: paper sizes, margins, header and footer templates with page numbers, one-page fit and print CSS.

By Simon Chiu · Updated

RenderScreenshot turns a URL or a string of HTML into a PDF with one HTTP request. Headless Chrome loads the page, applies your paper size, margins and header and footer templates, and the API returns the PDF bytes. You get invoices, reports, statements and archived web pages without a browser fleet to run.

curl -X POST https://api.renderscreenshot.com/v1/screenshot \
  -H "Authorization: Bearer $RS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "output": { "format": "pdf" },
    "pdf": { "paper": "a4", "margin": "15mm", "print_background": true }
  }' \
  --fail --output example.pdf

The same key also makes PNG, JPEG and WebP screenshots, so one integration covers "download as PDF" and "share as image". Every option on this page is listed in the PDF parameters reference. If you are still comparing services, our roundup of HTML to PDF APIs puts seven options side by side.

Four requests, four documents

Each request below is a complete POST body. Change the url (or swap it for html) and send it.

Document What the request does Body
A4 invoice with page numbers A4 paper, side margins, a footer that reads "Page 2 of 3" see header and footer templates
US Letter statement Letter is the default paper size, so only margins are set {"url": "...", "output": {"format": "pdf"}, "pdf": {"margin": "0.75in"}}
Wide dashboard export Landscape A4 with backgrounds, so chart fills print {"url": "...", "output": {"format": "pdf"}, "pdf": {"paper": "a4", "landscape": true, "print_background": true}}
One-page summary Scales the whole page down until it fits on one sheet {"url": "...", "output": {"format": "pdf"}, "pdf": {"paper": "letter", "fit_one_page": true}}

For the common case there is also a preset. "preset": "pdf_document" gives you Letter paper, background graphics and 1 cm margins, as listed in the presets reference.

How a PDF request flows

sequenceDiagram
    participant App as Your app
    participant API as RenderScreenshot API
    participant CF as Headless Chrome (Cloudflare)
    participant R2 as R2 storage + CDN
    App->>API: POST /v1/screenshot (url or html, output.format pdf, pdf options)
    API->>API: Auth, validation, credit check
    API->>CF: Load page, wait, print with paper, margins, templates
    CF->>R2: Store the PDF
    API-->>App: PDF bytes + X-Cache-URL + page metadata headers

Three response details matter for documents:

  • X-Cache-URL is a public CDN link to the same PDF. Store it on the invoice row or put it in an email, and nobody needs your key to open it. It lives for the cache TTL (24 hours by default; set cache.ttl up to 30 days).
  • X-Screenshot-Title and the other metadata headers come from the real page, so you can name the file after the document title without parsing the PDF. See response headers.
  • Content-Disposition is inline by default. Set output.filename to control the name a browser offers when someone saves it.

When you regenerate the same document, add "cache": { "available": true }. An identical request inside the TTL returns the stored PDF for 0 credits instead of rendering again (caching).

Chrome prints the pdf.header and pdf.footer strings into the page margins. Inside them, elements with these classes are filled in on each page: pageNumber, totalPages, date, title and url.

{
  "url": "https://example.com/invoices/2048",
  "output": { "format": "pdf", "filename": "invoice-2048" },
  "pdf": {
    "paper": "a4",
    "print_background": true,
    "margin": "16mm",
    "margin_bottom": "22mm",
    "header": "<span></span>",
    "footer": "<div style='font-size:9px;width:100%;text-align:center;color:#475569'>Page <span class='pageNumber'></span> of <span class='totalPages'></span></div>"
  }
}

We tested the template rules in Chromium 148 with Puppeteer before writing this section. Three of them catch nearly everyone:

  1. Set a font size inside the template. Our footer without a font-size printed at about 1 pt, a speck at the bottom of the page. Templates do not inherit your page's CSS, so style them inline.
  2. Leave a margin for the template. With a bottom margin of 0, Chrome drew "Page 1 of 2" on top of the last lines of body text. Give the side that holds a template at least 15 to 20 mm.
  3. Send a header when you only want a footer. When we passed only a footer template, Chrome printed its default header on every page: the date on the left and the document title on the right. An empty <span></span> header prevents that.

Images in templates should be data: URIs, because the template is not loaded like a normal page.

Your code, in six languages

The SDKs build the same request. These follow the SDK docs; we checked that each builder method exists in the current Node, Python and Ruby packages.

// Node.js 20+: npm install renderscreenshot
import { writeFile } from 'node:fs/promises';
import { Client, TakeOptions } from 'renderscreenshot';

const client = new Client(process.env.RS_API_KEY);
const pdf = await client.take(
  TakeOptions.url('https://example.com/invoices/2048')
    .format('pdf')
    .pdfPaperSize('a4')
    .pdfMargin('16mm')
    .pdfPrintBackground()
    .pdfHeader('<span></span>')
    .pdfFooter('<div style="font-size:9px;width:100%;text-align:center">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>')
);
await writeFile('invoice.pdf', pdf);
# Python 3.9+
import os
from renderscreenshot import Client, TakeOptions

client = Client(os.environ['RS_API_KEY'])
pdf = client.take(
    TakeOptions.url('https://example.com/invoices/2048')
    .format('pdf')
    .pdf_paper_size('a4')
    .pdf_margin('16mm')
    .pdf_print_background()
)
with open('invoice.pdf', 'wb') as f:
    f.write(pdf)
# Ruby 3.0+
require 'renderscreenshot'

client = RenderScreenshot.client(ENV.fetch('RS_API_KEY'))
pdf = client.take(
  RenderScreenshot::TakeOptions.url('https://example.com/invoices/2048')
    .format('pdf').pdf_paper_size('a4').pdf_margin('16mm').pdf_print_background
)
File.binwrite('invoice.pdf', pdf)
<?php
use RenderScreenshot\Client;
use RenderScreenshot\TakeOptions;

$client = new Client(getenv('RS_API_KEY'));
$pdf = $client->take(
    TakeOptions::url('https://example.com/invoices/2048')
        ->format('pdf')->pdfPaperSize('a4')->pdfMargin('16mm')->pdfPrintBackground()
);
file_put_contents('invoice.pdf', $pdf);

The Go and Java snippets show the call only; the imports are in the Go and Java SDK pages. For a complete Java app, see HTML to PDF in Spring Boot.

ctx := context.Background()
client, err := rs.New(os.Getenv("RS_API_KEY"))
if err != nil { log.Fatal(err) }
pdf, err := client.Take(ctx, rs.URL("https://example.com/invoices/2048").
    Format(rs.FormatPDF).PDFPaperSize(rs.PaperA4).PDFMarginUniform("16mm").PDFPrintBackground())
if err != nil { log.Fatal(err) }
os.WriteFile("invoice.pdf", pdf, 0o644)
Client client = new Client(System.getenv("RS_API_KEY"));
byte[] pdf = client.take(
    TakeOptions.url("https://example.com/invoices/2048")
        .format("pdf").pdfPaperSize("a4").pdfMargin("16mm").pdfPrintBackground());
Files.write(Path.of("invoice.pdf"), pdf);

fit_one_page, prefer_css_page_size and the per-side margins are also plain JSON fields if you call the HTTP API directly.

Most "the PDF looks wrong" tickets are CSS, not the API. Work through this list before you change any parameter:

  • [ ] Pick screen or print styles on purpose. If your page has @media print rules, send "browser": { "media": "print" }. If it has none and the PDF should look like the screen, leave the default. See the print media guide.
  • [ ] Keep backgrounds on for designed documents. In our Chromium test, turning printBackground off removed the blue fill from an invoice's table header, and its white text came out grey. print_background is true by default here; only turn it off for plain text output.
  • [ ] Decide who owns the page size. Chrome ignores @page { size: A5 } unless you ask it not to: our test PDF came out Letter until preferCSSPageSize was set, then A5. The API field is pdf.prefer_css_page_size.
  • [ ] Control breaks. Use break-before: page on chapter headings, break-inside: avoid on table rows and total boxes, and thead { display: table-header-group } so the table header repeats on every page.
  • [ ] Wait for fonts and data. A PDF prints when the wait condition passes. For web fonts and pages that fetch data, send "wait": { "until": "networkidle" } or wait for a selector that appears when the data is in (wait options).
  • [ ] Expect page counts to move between machines. The same report printed 6 pages on macOS and 11 pages in a Linux container, because a different serif font was installed. Ship your own web fonts if page breaks must not move.

Pricing

One successful render is one credit, whether the PDF is one page or two hundred. Failed renders are free, and there is no overage: at the limit, requests stop with insufficient_credits until the period resets or you upgrade (credits and billing).

Plan Credits per month Price
Free 150 $0, no credit card
Starter 2,500 $10
Growth 12,000 $40
Scale 60,000 $100

A team that sends 3,000 invoices a month needs the Growth plan, which works out at $13.33 per 1,000 PDFs at that volume. Current prices are always on the pricing page.

When to use something else

  • Books, footnotes and PDF/UA output. A print engine such as Prince implements paged-media CSS that Chrome does not. See our DocRaptor alternative page for where that matters.
  • Pages on a private network. The renderer only loads public addresses, so intranet pages need a renderer inside your network.
  • You still run wkhtmltopdf. Start with the wkhtmltopdf alternative guide: it shows what its engine drops and has drop-in replacements for Ruby, PHP and Python.

Get a free API key and print your first document.

Chromium findings on this page come from local tests on 2026-10-18 with Node 22.18.0, Puppeteer 24.43.1 and Chrome 148.0.7778.97 on macOS (Apple M1 Pro), and with Browserless 2.57.0 (Chrome 156) in Docker for the page-count comparison. They describe Chrome's print engine, not API latency.

Frequently asked questions

Can I send HTML instead of a URL?
Yes. Send the document in the html field of a POST request. Set html_base_url when the HTML loads stylesheets, fonts or images with relative paths.
How do I print page numbers?
Put a footer template in pdf.footer with span elements that have the classes pageNumber and totalPages. Give the template an explicit font-size and leave a bottom margin for it, or it prints too small or on top of your content.
Does a 40-page PDF cost more than a 1-page PDF?
No. One successful render is one credit, whatever the page count, file size or format. Failed renders are not charged.
Why does my PDF look different from the page in my browser?
Usually one of three things: the print stylesheet is not applied (set browser.media to print), backgrounds are off, or a web font had not loaded when the page printed. The checklist on this page covers each one.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.