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"]);
});
Add your API key to appsettings.json (or environment variables in production):
{
"PageBolt": {
"ApiKey": "your_api_key_here"
}
}
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();
}
}
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");
}
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();
}
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");
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");
}
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);
}
}
}
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();
}
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;
}
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)