DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

Screenshot API for ASP.NET Core: Screenshots and PDFs from C# Without Puppeteer

Screenshot API for ASP.NET Core: Screenshots and PDFs from C# Without Puppeteer

If you need to capture screenshots or generate PDFs inside an ASP.NET Core application, the self-hosting path is painful. Puppeteer Sharp pulls in Chromium. Selenium requires a browser driver. Both options add deployment complexity, memory pressure, and maintenance overhead to your .NET service.

A REST API handles all of that. You POST a URL or HTML, get back a binary file. This guide shows how to integrate the PageBolt API into ASP.NET Core using HttpClient — including screenshots, HTML-to-PDF, invoices, and visual monitoring.

Why Not Puppeteer Sharp?

Puppeteer Sharp works, but it comes with tradeoffs:

  • Docker image bloat — Chromium adds 300–600 MB to your container.
  • Memory leaks — Long-running browser instances in a hosted service need careful lifecycle management.
  • Blocked renders — Many sites detect headless Chrome and serve different content.
  • Platform differences — Linux font rendering and Windows rendering differ. CI screenshots look different from local.

A screenshot API sidesteps all of this. Your .NET service stays lean. The API handles the browser.

Setup: Register HttpClient in Program.cs

Register a named HttpClient with your base address and API key:

builder.Services.AddHttpClient("pagebolt", client =>
{
    client.BaseAddress = new Uri("https://pagebolt.dev/api/v1/");
    client.DefaultRequestHeaders.Add("x-api-key", builder.Configuration["PageBolt:ApiKey"]);
});
Enter fullscreen mode Exit fullscreen mode

Add your API key to appsettings.json (or environment variables in production):

{
  "PageBolt": {
    "ApiKey": "your_api_key_here"
  }
}
Enter fullscreen mode Exit fullscreen mode

Taking a Screenshot

A basic screenshot method using IHttpClientFactory:

public class ScreenshotService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public ScreenshotService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<byte[]> CaptureAsync(string url, bool fullPage = false)
    {
        var client = _httpClientFactory.CreateClient("pagebolt");

        var payload = new
        {
            url,
            fullPage,
            blockBanners = true,
            blockAds = true
        };

        var response = await client.PostAsJsonAsync("screenshot", payload);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadAsByteArrayAsync();
    }
}
Enter fullscreen mode Exit fullscreen mode

Call it from a controller and return the file directly:

[HttpGet("capture")]
public async Task<IActionResult> Capture([FromQuery] string url)
{
    var bytes = await _screenshotService.CaptureAsync(url, fullPage: true);
    return File(bytes, "image/png", "screenshot.png");
}
Enter fullscreen mode Exit fullscreen mode

HTML to PDF from ASP.NET Core

The /pdf endpoint accepts raw HTML and returns a PDF binary. This is the correct pattern for invoice generation, reports, and certificates — render your Razor template to a string, then POST it to the API.

public async Task<byte[]> GeneratePdfAsync(string html)
{
    var client = _httpClientFactory.CreateClient("pagebolt");

    var payload = new
    {
        html,
        pdfOptions = new
        {
            format = "A4",
            printBackground = true,
            margin = new { top = "20mm", bottom = "20mm", left = "15mm", right = "15mm" }
        }
    };

    var response = await client.PostAsJsonAsync("pdf", payload);
    response.EnsureSuccessStatusCode();

    return await response.Content.ReadAsByteArrayAsync();
}
Enter fullscreen mode Exit fullscreen mode

To render a Razor view to string before sending:

// Inject IRazorViewEngine + ITempDataProvider + IServiceProvider
var html = await _razorRenderer.RenderViewToStringAsync("Invoices/Template", model);
var pdfBytes = await _pdfService.GeneratePdfAsync(html);
return File(pdfBytes, "application/pdf", $"invoice-{model.InvoiceNumber}.pdf");
Enter fullscreen mode Exit fullscreen mode

Use Case 1: Invoice Generation

Pattern: render an invoice Razor view to HTML, POST to /pdf, stream the PDF back to the browser or upload to Azure Blob Storage.

[HttpGet("invoice/{id}")]
public async Task<IActionResult> DownloadInvoice(int id)
{
    var invoice = await _db.Invoices.FindAsync(id);
    if (invoice == null) return NotFound();

    var html = await _razorRenderer.RenderViewToStringAsync("Invoices/Pdf", invoice);
    var pdfBytes = await _pdfService.GeneratePdfAsync(html);

    Response.Headers.Append("Content-Disposition", $"attachment; filename=\"invoice-{invoice.Number}.pdf\"");
    return File(pdfBytes, "application/pdf");
}
Enter fullscreen mode Exit fullscreen mode

No Puppeteer Sharp. No wkhtmltopdf. No headless Chrome in your container.

Use Case 2: Visual Website Monitoring

Schedule a screenshot job with a background service. Compare against a baseline to detect layout breaks or error pages after a deploy.

public class MonitoringJob : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            var bytes = await _screenshotService.CaptureAsync("https://yourapp.com");
            var path = $"snapshots/{DateTime.UtcNow:yyyy-MM-dd-HH}.png";
            await File.WriteAllBytesAsync(path, bytes, stoppingToken);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Compare consecutive snapshots with a pixel-diff library to catch silent visual regressions.

Use Case 3: Open Graph Images

Generate og:image cards for blog posts by screenshotting an OG template page. Call this on publish and cache the result in your CDN.

public async Task<byte[]> GenerateOgImageAsync(string title, string author)
{
    var client = _httpClientFactory.CreateClient("pagebolt");

    var templateUrl = $"https://yourapp.com/og-template" +
                      $"?title={Uri.EscapeDataString(title)}" +
                      $"&author={Uri.EscapeDataString(author)}";

    var payload = new
    {
        url = templateUrl,
        viewportWidth = 1200,
        viewportHeight = 630,
        format = "jpeg",
        quality = 90
    };

    var response = await client.PostAsJsonAsync("screenshot", payload);
    response.EnsureSuccessStatusCode();

    return await response.Content.ReadAsByteArrayAsync();
}
Enter fullscreen mode Exit fullscreen mode

Handling Errors

The API returns standard HTTP status codes. Wrap calls in a try/catch and surface the response body on failures:

try
{
    var response = await client.PostAsJsonAsync("screenshot", payload);

    if (!response.IsSuccessStatusCode)
    {
        var error = await response.Content.ReadAsStringAsync();
        _logger.LogError("PageBolt error {Status}: {Body}", (int)response.StatusCode, error);
        throw new InvalidOperationException($"Screenshot failed: {error}");
    }

    return await response.Content.ReadAsByteArrayAsync();
}
catch (HttpRequestException ex)
{
    _logger.LogError(ex, "Network error calling PageBolt API");
    throw;
}
Enter fullscreen mode Exit fullscreen mode

Free Tier

PageBolt's free plan includes 100 requests per month with no credit card required. That's enough to build and test a full integration — screenshots, PDFs, and OG image generation — before you go near a billing page.

Get started: pagebolt.dev

Top comments (0)