How to take screenshots and generate PDFs in C# and .NET

Published: (February 26, 2026 at 01:08 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

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.

// Program.cs
builder.Services.AddHttpClient();
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)

The no‑browser approach shines here. 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;
}

Author

Custodia-Admin

All code snippets are ready to drop into a .NET project. No external browsers required.

No browser binary. No filesystem writes. Works on Consumption plan.

Get started in 2 minutes – free 100 requests/month, no credit card required

0 views
Back to Blog

Related posts

Read more »

Every service I build will die

And that's exactly the point. I'm a senior software engineer at Ontime Payments, a fintech startup enabling direct‑from‑salary bill payments. We've deliberately...