How to Take Screenshots and Generate PDFs in C# and .NET
PuppeteerSharp and Playwright for .NET both download a ~200MB Chromium binary on first run and fail in restricted environments like Azure Functions Consumption plan. Here's the simpler path: HttpClient, System.Text.Json, binary response. No browser download.
The usual .NET options for screenshots and PDFs are PuppeteerSharp or Playwright for .NET — both download a bundled Chromium binary on first run (~200MB), require a writable filesystem, and don't work in restricted environments like Azure Functions on the Consumption plan or Lambda.
Here's the simpler path: HttpClient, System.Text.Json, binary response. No browser download.
Screenshot from a URL
using System.Net.Http;
using System.Text;
using System.Text.Json;
public class PageBoltClient
{
private static readonly HttpClient _http = new();
private readonly string _apiKey = Environment.GetEnvironmentVariable("PAGEBOLT_API_KEY")!;
private const string BaseUrl = "https://pagebolt.dev/api/v1";
public async Task<byte[]> ScreenshotAsync(string url)
{
var body = JsonSerializer.Serialize(new
{
url,
fullPage = true,
blockBanners = true
});
using var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/screenshot");
request.Headers.Add("x-api-key", _apiKey);
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
using var response = await _http.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync();
}
}
PDF from a URL
public async Task<byte[]> PdfFromUrlAsync(string url)
{
var body = JsonSerializer.Serialize(new { url, blockBanners = true });
using var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/pdf");
request.Headers.Add("x-api-key", _apiKey);
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
using var response = await _http.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync();
}
PDF from HTML (replacing PuppeteerSharp / wkhtmltopdf)
public async Task<byte[]> PdfFromHtmlAsync(string html)
{
var body = JsonSerializer.Serialize(new { html });
using var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/pdf");
request.Headers.Add("x-api-key", _apiKey);
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
using var response = await _http.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync();
}
System.Text.Json.JsonSerializer.Serialize handles all escaping — no manual string-building required.
Register as a singleton in DI (recommended)
// Program.cs
builder.Services.AddHttpClient<PageBoltClient>();
builder.Services.AddSingleton<PageBoltClient>();
// PageBoltClient.cs — constructor injection version
public class PageBoltClient
{
private readonly HttpClient _http;
private readonly string _apiKey;
private const string BaseUrl = "https://pagebolt.dev/api/v1";
public PageBoltClient(HttpClient http, IConfiguration config)
{
_http = http;
_apiKey = config["PageBolt:ApiKey"] ?? throw new InvalidOperationException("PageBolt:ApiKey not configured");
}
public async Task<byte[]> ScreenshotAsync(string url) { /* ... */ }
public async Task<byte[]> PdfFromHtmlAsync(string html) { /* ... */ }
}
ASP.NET Core controller
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/invoices")]
public class InvoiceController : ControllerBase
{
private readonly PageBoltClient _pagebolt;
private readonly IInvoiceService _invoices;
private readonly IRazorViewRenderer _renderer;
public InvoiceController(
PageBoltClient pagebolt,
IInvoiceService invoices,
IRazorViewRenderer renderer)
{
_pagebolt = pagebolt;
_invoices = invoices;
_renderer = renderer;
}
[HttpGet("{id}/pdf")]
public async Task<IActionResult> DownloadPdf(int id)
{
var invoice = await _invoices.GetByIdAsync(id);
var html = await _renderer.RenderAsync("Invoice", invoice);
var pdf = await _pagebolt.PdfFromHtmlAsync(html);
return File(pdf, "application/pdf", $"invoice-{id}.pdf");
}
[HttpGet("{id}/screenshot")]
public async Task<IActionResult> GetScreenshot(int id)
{
var invoice = await _invoices.GetByIdAsync(id);
var image = await _pagebolt.ScreenshotAsync($"https://yourapp.com/invoices/{id}");
return File(image, "image/png", $"invoice-{id}.png");
}
}
Azure Functions (Consumption plan)
This is where the no-browser approach matters most. PuppeteerSharp and Playwright fail on Azure Functions Consumption plan because there's no writable directory for the Chromium binary download. An HTTP call works fine:
[Function("GenerateInvoicePdf")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "invoices/{id}/pdf")] HttpRequestData req,
int id)
{
var html = await _renderer.RenderAsync("Invoice", id);
var pdf = await _pagebolt.PdfFromHtmlAsync(html);
var response = req.CreateResponse(HttpStatusCode.OK);
response.Headers.Add("Content-Type", "application/pdf");
response.Headers.Add("Content-Disposition", $"attachment; filename=\"invoice-{id}.pdf\"");
await response.WriteBytesAsync(pdf);
return response;
}
No browser binary. No filesystem writes. Works on Consumption plan.
Get Started Free
100 requests/month, no credit card
Screenshots, PDFs, and video recordings from your .NET app — no PuppeteerSharp, no Chromium download, works on Azure Functions Consumption plan.
Get Your Free API Key →