The C# screenshot API from ScreenshotRun lets you capture any webpage with a single HttpClient call. No Chromium binaries to install, no PuppeteerSharp dependency to manage, no Selenium drivers to version-match. Pass a URL to the REST endpoint and get back a PNG, JPEG, WebP, AVIF, or PDF.
Most .NET developers who automate screenshots start with PuppeteerSharp or Selenium. Both work on a local machine. Deploy that same code to a Docker container or an Azure App Service and you end up managing font packages, shared memory limits, zombie browser processes. A service that ran smoothly now eats 500 MB of RAM per browser instance. The API approach skips all of that.
First C# screenshot in 30 seconds
Create an account in the dashboard and grab your API key. The free plan includes 200 captures per month, no credit card required. Then run this in a console app:
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer YOUR_API_KEY");
var response = await client.GetAsync(
"https://api.screenshotrun.com/v1/screenshots/capture"
+ "?url=https://github.com&format=png&response_type=image"
);
response.EnsureSuccessStatusCode();
var bytes = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync("github.png", bytes);
Console.WriteLine($"Saved: github.png ({bytes.Length / 1024} KB)");
No Chromium download, no driver version to match. Our servers launch a headless Chrome instance, wait for the page to finish rendering (JavaScript execution, lazy-loaded assets, web font loading), and stream the result back as a byte array your HttpClient can read directly.
Production-ready helper method
A reusable method with configurable parameters:
using System.Web;
public static async Task<byte[]> CaptureScreenshotAsync(
HttpClient client,
string targetUrl,
string format = "png",
int width = 1280,
int height = 720,
bool fullPage = false)
{
var query = HttpUtility.ParseQueryString(string.Empty);
query["url"] = targetUrl;
query["format"] = format;
query["width"] = width.ToString();
query["height"] = height.ToString();
query["full_page"] = fullPage.ToString().ToLower();
query["response_type"] = "image";
var response = await client.GetAsync(
$"https://api.screenshotrun.com/v1/screenshots/capture?{query}"
);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync();
}
Call it from anywhere in your application:
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization",
$"Bearer {Environment.GetEnvironmentVariable("SCREENSHOTRUN_KEY")}");
var screenshot = await CaptureScreenshotAsync(
client, "https://github.com",
format: "webp", width: 1920, fullPage: true
);
await File.WriteAllBytesAsync("github-full.webp", screenshot);
Console.WriteLine("Captured full-page WebP screenshot");
The API key comes from an environment variable. This keeps secrets out of source control and makes rotation a config change, not a code change.
HttpClient screenshot API integration in ASP.NET Core
Creating new HttpClient() in a loop is a known problem in .NET. It exhausts sockets and triggers SocketException under load. ASP.NET Core solves this with IHttpClientFactory, which pools and reuses connections automatically.
Register a named client in Program.cs:
builder.Services.AddHttpClient("ScreenshotRun", client =>
{
client.BaseAddress = new Uri("https://api.screenshotrun.com/v1/screenshots/");
client.DefaultRequestHeaders.Add("Authorization",
$"Bearer {builder.Configuration["ScreenshotRun:ApiKey"]}");
client.Timeout = TimeSpan.FromSeconds(60);
});
Then inject the factory wherever you need screenshots:
public class ScreenshotService
{
private readonly IHttpClientFactory _httpClientFactory;
public ScreenshotService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<byte[]> CaptureAsync(
string url, string format = "png", int width = 1280)
{
var client = _httpClientFactory.CreateClient("ScreenshotRun");
var query = HttpUtility.ParseQueryString(string.Empty);
query["url"] = url;
query["format"] = format;
query["width"] = width.ToString();
query["response_type"] = "image";
var response = await client.GetAsync($"capture?{query}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync();
}
}
The factory handles DNS rotation and connection pooling. Your code creates a client per request without worrying about socket exhaustion. This is the pattern Microsoft recommends for external HTTP calls in ASP.NET Core.
Available parameters
Every parameter goes into the query string of a GET request. These cover most production scenarios:
| Parameter | Type | Default | Description |
|---|---|---|---|
url | string | required | The webpage to capture |
format | string | png | Output: png, jpeg, webp, avif, pdf |
width | int | 1280 | Viewport width in pixels |
height | int | 720 | Viewport height in pixels |
full_page | bool | false | Capture the entire scrollable page |
device | string | desktop | Device type: desktop, mobile, tablet |
retina | bool | false | Render at 2x pixel density |
dark_mode | bool | false | Force dark color scheme |
selector | string | CSS selector to capture a specific element | |
delay | int | 0 | Wait N milliseconds before capturing |
block_cookies | bool | false | Remove cookie consent banners |
block_ads | bool | false | Remove advertisements |
block_chats | bool | false | Remove live chat widgets |
css | string | Inject custom CSS before capture | |
js | string | Execute JavaScript before capture |
The full parameter reference is in the API documentation.
Capture screenshots in dark mode, by selector, or with custom CSS
Dark mode
Set dark_mode=true and the API applies prefers-color-scheme: dark before the page loads. Any site that respects this media query renders its dark theme automatically:
var query = HttpUtility.ParseQueryString(string.Empty);
query["url"] = "https://github.com";
query["dark_mode"] = "true";
query["response_type"] = "image";
Element capture
Pass a CSS selector to isolate a specific part of the page. The API crops the output to that element's bounding box:
query["url"] = "https://github.com/anthropics";
query["selector"] = ".js-repo-list";
query["response_type"] = "image";
CSS injection
Hide a banner, swap fonts, or adjust layout before the screenshot fires:
query["url"] = "https://example.com";
query["css"] = ".promo-banner { display: none !important; } body { font-family: Inter, sans-serif; }";
query["response_type"] = "image";
All three work together in one request. A single call can capture a specific element in dark mode with custom styles applied.
Error handling and retries for the .NET screenshot client
Network calls fail. Target sites time out, rate limits kick in, and servers occasionally return 5xx. Production code needs retry logic:
public static async Task<byte[]> CaptureWithRetryAsync(
HttpClient client, string url, int maxRetries = 2)
{
var query = HttpUtility.ParseQueryString(string.Empty);
query["url"] = url;
query["format"] = "png";
query["response_type"] = "image";
for (int attempt = 0; attempt <= maxRetries; attempt++)
{
var response = await client.GetAsync(
$"https://api.screenshotrun.com/v1/screenshots/capture?{query}"
);
if (response.IsSuccessStatusCode)
return await response.Content.ReadAsByteArrayAsync();
if (response.StatusCode == (System.Net.HttpStatusCode)429)
{
var retryAfter = response.Headers.RetryAfter?.Delta
?? TimeSpan.FromSeconds(5);
await Task.Delay(retryAfter);
continue;
}
if ((int)response.StatusCode >= 500 && attempt < maxRetries)
{
await Task.Delay(TimeSpan.FromSeconds(1 * (attempt + 1)));
continue;
}
response.EnsureSuccessStatusCode(); // throws on 4xx
}
throw new HttpRequestException("Max retries exceeded");
}
Rate-limit responses (429) include a Retry-After header. The code respects it instead of guessing a wait time. Server errors get retried with increasing backoff. Client errors (invalid parameters, bad credentials) throw immediately because retrying those changes nothing.
Minimal API proxy endpoint
A common ASP.NET Core pattern: the frontend requests screenshots, but your API key should never reach the browser. A Minimal API endpoint acts as a thin proxy:
app.MapGet("/api/screenshot", async (
string url,
string format,
int width,
IHttpClientFactory factory) =>
{
var client = factory.CreateClient("ScreenshotRun");
var query = HttpUtility.ParseQueryString(string.Empty);
query["url"] = url;
query["format"] = format ?? "png";
query["width"] = (width > 0 ? width : 1280).ToString();
query["response_type"] = "image";
var response = await client.GetAsync($"capture?{query}");
if (!response.IsSuccessStatusCode)
return Results.StatusCode((int)response.StatusCode);
var bytes = await response.Content.ReadAsByteArrayAsync();
var contentType = response.Content.Headers.ContentType?.ToString() ?? "image/png";
return Results.File(bytes, contentType);
});
Your client calls /api/screenshot?url=https://example.com&format=png&width=1280 and gets the image back directly. The API key never leaves the server. Combine this with [OutputCache] or response caching middleware to avoid duplicate API calls for the same URL.
Screenshot API vs. PuppeteerSharp and Selenium for .NET
PuppeteerSharp and Selenium are solid choices for browser automation: login flows, multi-step form testing, full E2E test suites. When you just need to convert URLs into images, maintaining that stack for screenshots alone becomes hard to justify.
| Self-hosted (PuppeteerSharp / Selenium) | ScreenshotRun API | |
|---|---|---|
| Setup | Install Chromium, match driver versions, configure fonts | One HTTP GET request |
| Dependencies | PuppeteerSharp + Chromium binary or Selenium.WebDriver + ChromeDriver | HttpClient (built into .NET) |
| Memory per capture | 200-500 MB per browser instance | Zero on your server |
| Scaling | Thread pools, browser reuse, OOM monitoring, zombie process cleanup | Handled by the API |
| Cookie banners | Custom dismiss scripts per site, ongoing maintenance | One parameter: block_cookies=true |
| Font rendering | Install font packages on every server and container | Consistent across all captures |
| Docker image size | 1-2 GB with Chromium | Your app stays at its normal size |
| Cost | Server CPU/RAM + engineering time | Free tier (200/month), then usage-based |
If your application needs a real browser for interactive flows (login, form wizards, E2E tests), PuppeteerSharp or Selenium is the right tool. If the job is generating images from URLs without hosting Chrome yourself, one HttpClient.GetAsync call replaces hundreds of lines of browser management code.
Common .NET screenshot use cases: website change monitoring, automated dashboard reports, and legal evidence capture.
FAQ#
Use HttpClient to call a screenshot API endpoint. Send the target URL as a query parameter, include your API key in the Authorization header, and read the response body as a byte array. The entire process takes about five lines of code and requires no browser binaries on your server.
Yes. The API is a standard REST endpoint. Any .NET version that supports HttpClient works, including .NET Framework 4.6.1+ and all modern .NET releases. The code examples on this page target .NET 6+ syntax (top-level statements, global usings), but the API calls themselves are the same across versions.
No. The API is a simple GET request. HttpClient ships with .NET, so there are zero additional packages to install. If you prefer a typed client, wrap the calls in a service class and register it with IHttpClientFactory as shown in the ASP.NET Core section above.
Set full_page=true in the query string. The API scrolls the entire page, waits for lazy-loaded content to appear, then stitches the result into a single image. This works with PNG, JPEG, WebP, and AVIF formats.
The free tier gives you 200 captures per month with all parameters available. Paid plans scale by volume. Compare that to the engineering time spent maintaining Chromium on your servers: driver updates, memory tuning, font installation, Docker image bloat. For most teams the API costs less than the infrastructure it replaces.
Absolutely. The API is a single HTTP GET call that returns an image. No file system access, no browser process, no shared memory requirements. Cloud functions that run PuppeteerSharp regularly hit memory limits and cold-start timeouts. An HTTP call to a screenshot API finishes in seconds regardless of the function's memory allocation.