HTML to PDF in C# / .NET

Three ways to turn HTML into a real, text-selectable PDF from C#, all run on the same invoice template on .NET 10, plus the NuGet packages you should skip. Tested with PuppeteerSharp 25.14 and Microsoft.Playwright 1.63.

Quick answer

OptionPick it when
PuppeteerSharp
.NET port of Puppeteer
You want the most direct Chrome control and an in-code browser download (BrowserFetcher).
Playwright for .NET
By Microsoft, same Chromium print engine
You already write Playwright tests in .NET, or want built-in auto-waiting. Same PDF options.
An API
Chromium on someone else's server
Azure Functions, App Service, slim containers, or apps where a ~150 MB browser per instance isn't worth running.

Skip these: DinkToPdf, Rotativa and other wkhtmltopdf wrappers ship a 2012-era WebKit whose project is archived (wkhtmltopdf alternative); iTextSharp's old HTMLWorker is deprecated and ignores most CSS; PdfSharp and QuestPDF are good libraries but build PDFs from code and don't read HTML. Commercial HTML-to-PDF libraries (IronPDF, SelectPdf and others) work but need a paid license per developer or deployment, so read their terms first.

The test template

Save this as invoice.html next to your project:

<!doctype html>
<html><head><meta charset="utf-8"><style>
  body { font-family: sans-serif; }
  .top { display: flex; justify-content: space-between; background: #eef2ff; padding: 12px; }
  table { width: 100%; border-collapse: collapse; }
  td, th { border-bottom: 1px solid #ddd; padding: 6px; text-align: left; }
  tr { break-inside: avoid; }
</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. PuppeteerSharp

dotnet new console -n InvoicePdf && cd InvoicePdf
dotnet add package PuppeteerSharp

Replace Program.cs and run dotnet run:

using PuppeteerSharp;
using PuppeteerSharp.Media;

await new BrowserFetcher().DownloadAsync(); // downloads Chrome once, then reuses it
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true, Args = ["--no-sandbox"] });
await using var page = await browser.NewPageAsync();
await page.SetContentAsync(await File.ReadAllTextAsync("invoice.html"));
await page.PdfAsync("invoice.pdf", new PdfOptions
{
    Format = PaperFormat.A4,
    PrintBackground = true,
    MarginOptions = new MarginOptions { Top = "20mm", Bottom = "20mm", Left = "15mm", Right = "15mm" },
    DisplayHeaderFooter = true,
    HeaderTemplate = "<span></span>",
    FooterTemplate = "<div style='font-size:9px;width:100%;text-align:right;padding-right:15mm'>" +
        "Page <span class='pageNumber'></span> of <span class='totalPages'></span></div>",
});

Result: an A4 PDF with the blue header bar, title and due date side by side, selectable text and "Page 1 of 1" at the bottom right. For a live page, use await page.GoToAsync("https://example.com", WaitUntilNavigation.Networkidle0) instead of SetContentAsync. Things that trip people up:

2. Playwright for .NET

dotnet add package Microsoft.Playwright
dotnet build
pwsh bin/Debug/net10.0/playwright.ps1 install chromium

No PowerShell? Call the installer from code once: Microsoft.Playwright.Program.Main(["install", "chromium"]);. Then:

using Microsoft.Playwright;

using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
await page.SetContentAsync(await File.ReadAllTextAsync("invoice.html"));
await page.PdfAsync(new PagePdfOptions
{
    Path = "invoice-pw.pdf",
    Format = "A4",
    PrintBackground = true,
    Margin = new Margin { Top = "20mm", Bottom = "20mm", Left = "15mm", Right = "15mm" },
    DisplayHeaderFooter = true,
    HeaderTemplate = "<span></span>",
    FooterTemplate = "<div style='font-size:9px;width:100%;text-align:right;padding-right:15mm'>" +
        "Page <span class='pageNumber'></span> of <span class='totalPages'></span></div>",
});

The output matches PuppeteerSharp's: both drive Chromium's print-to-PDF, so the options map one to one. PDF export only works in Chromium, not in Firefox or WebKit. Playwright passes --no-sandbox itself. Using a .NET 10 file-based app (dotnet run app.cs)? Those disable reflection-based JSON, which Playwright needs, so add #:property JsonSerializerIsReflectionEnabledByDefault=true at the top, or it fails with "Reflection-based serialization has been disabled".

3. An API (no browser to install)

Shotmint renders in the same Chromium, but your app only makes an HTTP call: no browser download, no fonts, nothing to keep alive. No NuGet packages needed:

using System.Text;
using System.Text.Json.Nodes;

using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(90) };
http.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("SHOTMINT_KEY"));

var body = new JsonObject
{
    ["html"] = await File.ReadAllTextAsync("invoice.html"),
    ["format"] = "A4",
    ["margin"] = "20mm",
}.ToJsonString();

var res = await http.PostAsync("https://shotmint.creasework.com/v1/pdf",
    new StringContent(body, Encoding.UTF8, "application/json"));
if (!res.IsSuccessStatusCode)
    throw new Exception($"Render failed ({(int)res.StatusCode}): {await res.Content.ReadAsStringAsync()}");

await File.WriteAllBytesAsync("invoice-api.pdf", await res.Content.ReadAsByteArrayAsync());

JsonObject escapes the HTML for you, so a Razor-rendered string can go straight into html, and it also works in trimmed and Native AOT builds. For a live page, send url instead. Other options: landscape, format (A3, A4, A5, Letter, Legal, Tabloid), scale, print_background (on by default) and screen_media. There is no header/footer template option: for page numbers, use CSS @page { @bottom-right { content: "Page " counter(page) " of " counter(pages) } }, which current Chromium supports. In ASP.NET Core, register the client with IHttpClientFactory and return the bytes with Results.File(bytes, "application/pdf", "invoice.pdf"). Images and fonts must be reachable from the internet (or inlined as data URIs). Screenshots from C#: website screenshots in C#.

Side by side

FeatureSupport
RenderingAll three: Chromium, flexbox, grid, JavaScript, web fonts
Page numbersPuppeteerSharp and Playwright: FooterTemplate or CSS
API: CSS @page margin boxes
InstallAPI: nothing (built-in HttpClient)
PuppeteerSharp and Playwright: plus a ~150 MB Chromium and system libraries
Azure Functions, App ServiceAPI: any plan
Self-hosted: needs a container image with Chromium and fonts
Private pagesPuppeteerSharp and Playwright: can reach localhost and your VPN
API: public URLs or HTML you send

Which one?

If your app can run a browser, PuppeteerSharp or Playwright are free and give you full control; pick the one your team already knows. If it can't (consumption-plan Functions, a small container) or you'd rather not keep Chromium patched and fed with fonts, use the API. Want to see the output first? Paste your HTML into the free HTML to PDF converter; no key needed. Other languages: Node.js, Python.

Try it with 100 free renders

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).

Get a free API key Full API reference