HTML to PDF in PowerShell

PowerShell has no HTML renderer of its own, so a script that turns HTML into a PDF either drives a browser that's already installed (Chrome or Edge, headless) or sends the HTML to an API. We ran both in PowerShell 7.6 with Chrome 155, on the same invoice, and measured what each default got wrong. Every script below was run exactly as shown.

Quick answer

OptionPick it when
Headless Chrome or Edge
--print-to-pdf, nothing to install on Windows
The machine already has Chrome or Edge and you convert local files now and then.
An API
Invoke-RestMethod, no browser
The script runs on a server, in a scheduled task or in Azure Automation, where you'd rather not install and patch a browser.

Skip these: wkhtmltopdf (archived, no flexbox or grid: wkhtmltopdf alternative) and Word's COM object (Word.Application opening the HTML and saving as PDF), which needs Office installed, isn't supported in unattended server use by Microsoft, and lays out HTML like Word, not like a browser.

The test template

Save this as invoice.html next to your script. It asks for A4 in CSS and has a dark header bar:

<!doctype html>
<html><head><meta charset="utf-8"><style>
  @page { size: A4; margin: 20mm 15mm; }
  body { font-family: sans-serif; }
  .top { display: flex; justify-content: space-between; align-items: center;
         background: #1e3a8a; color: #fff; padding: 12px; }
  table { width: 100%; border-collapse: collapse; }
  td, th { border-bottom: 1px solid #ddd; padding: 6px; text-align: left; }
</style></head><body>
  <div class="top"><h1>Invoice #1042</h1><p>Due: 2026-11-01</p></div>
  <table><tr><th>Item</th><th>Price</th></tr>
    <tr><td>Consulting (3h)</td><td>€450.00</td></tr>
    <tr><td>Hosting, Oct.</td><td>€19.00</td></tr></table>
</body></html>

1. Headless Chrome or Edge

The one-liner most answers give is & $chrome --headless --print-to-pdf=invoice.pdf invoice.html. We ran it, and the PDF came out on A4 with the header bar, but with two lines we didn't ask for: the date and time (10/23/26, 9:31 AM) at the top, and the full local file path (file:///home/.../invoice.html) plus 1/1 at the bottom. Chrome prints its own header and footer unless you pass --no-pdf-header-footer; for a PDF you send to a customer, that leaks a path from your disk. This version fixes that and the other problems below:

# Chrome or Edge, already installed on most Windows machines
$chrome = "C:\Program Files\Google\Chrome\Application\chrome.exe"
# Edge: "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
if ($env:CHROME) { $chrome = $env:CHROME }

$in  = (Resolve-Path "invoice.html").Path
$out = Join-Path (Get-Location) "invoice.pdf"
# file:///C:/... with spaces escaped (a plain [Uri] cast gives $null on Linux/macOS)
$uri = [System.Uri]::new($in, [System.UriKind]::Absolute).AbsoluteUri

$flags = @(
  "--headless",
  "--no-pdf-header-footer",        # no date, title, file path, page number
  "--virtual-time-budget=5000",    # let scripts run before printing
  "--print-to-pdf=`"$out`"",
  $uri
)
if ($IsLinux) { $flags += "--no-sandbox" }

# Start-Process -Wait: on Windows, & $chrome returns before the PDF exists
Start-Process -FilePath $chrome -ArgumentList $flags -Wait -NoNewWindow
Write-Host "saved $out ($((Get-Item $out).Length) bytes)"

Result: a 1-page A4 PDF with the blue header bar and no date or path, in 2.9 s including Chrome's start. To convert a live page, set $uri to its URL. Our Chrome 155 kept background colors and the CSS page size from the command line, so there are no flags for those (unlike the DevTools print call, where both are off by default).

Why each line is there:

2. An API (no browser, one Invoke-RestMethod)

Shotmint renders in the same Chrome on its servers, with backgrounds on, A4 by default and no header or footer unless you ask for one. Get a free key (25 renders a month, no signup) and put it in $env:SHOTMINT_KEY:

$key  = $env:SHOTMINT_KEY
# -Raw reads one string; without it you get an array of lines
$html = Get-Content "invoice.html" -Raw -Encoding UTF8

$json = @{
  html = $html                  # or: url = "https://example.com"
  prefer_css_page_size = $true  # use @page size/margins from the CSS
  page_numbers = $true          # "1 / 3" footer
} | ConvertTo-Json

# Send bytes: Windows PowerShell 5.1 doesn't send a string body as UTF-8
$body = [System.Text.Encoding]::UTF8.GetBytes($json)

try {
  Invoke-RestMethod -Method Post -Uri "https://shotmint.creasework.com/v1/pdf" `
    -Headers @{ "X-API-Key" = $key } `
    -ContentType "application/json; charset=utf-8" `
    -Body $body -OutFile "invoice.pdf" -TimeoutSec 60
  Write-Host "saved invoice.pdf ($((Get-Item invoice.pdf).Length) bytes)"
} catch {
  # errors come back as JSON with a message
  Write-Error "render failed: $($_.Exception.Message) $($_.ErrorDetails.Message)"
  exit 1
}

Result: the same 1-page A4 invoice with the header bar, € signs intact and a "1/1" footer, in 0.76 s for the whole script. A wrong key ends in the catch with 401 (Unauthorized) and the JSON message Missing or invalid API key, and the script exits with code 1, so a scheduled task shows it as failed. One PDF is one credit, however many pages it has.

Screenshots of a list of websites

The same pattern takes a PNG of each URL in a list, for example to archive status pages or check a set of sites after a deploy:

$key  = $env:SHOTMINT_KEY
$urls = @("https://example.com", "https://en.wikipedia.org/wiki/PowerShell")

New-Item -ItemType Directory -Force -Path "shots" | Out-Null
foreach ($url in $urls) {
  $q = "url=" + [System.Uri]::EscapeDataString($url) + "&full_page=true"
  $file = Join-Path "shots" (($url -replace '^https?://', '' -replace '[^\w.-]', '_') + ".png")
  try {
    Invoke-RestMethod -Uri "https://shotmint.creasework.com/v1/screenshot?$q" `
      -Headers @{ "X-API-Key" = $key } -OutFile $file -TimeoutSec 90
    Write-Host "ok   $url -> $file"
  } catch {
    Write-Warning "fail $url : $($_.ErrorDetails.Message)"
  }
}

Result: shots/example.com.png (1280 × 843) and a full-page capture of the Wikipedia article on PowerShell, capped at 16,000 px tall, in 16.6 s for both. A URL that fails prints a warning with the API's message and the loop moves on. For hundreds of URLs, see bulk website screenshots.

Which one to pick

Related: HTML to PDF in C#, page numbers and footers, headless Chrome screenshots, background colors in PDFs, free HTML to PDF converter.

What it costs

One PDF or screenshot = one credit. Free keys get 25 renders a month, no signup. €5 buys 2,000 renders, credits never expire and there's no subscription.

Try it with 25 free renders a month

No signup, no card. Get a key in one click, then buy credits only if you need more (from €5 for 2,000, never expire). From a script, an AI agent, Make or Zapier, POST /v1/keys/free (no body) returns a key as JSON (openapi.json, no-code recipes).

Get a free API key Full API reference

More guides: HTML to PDF and HTML to image

All guides: guides · integrations · comparisons