Need to capture website screenshots from a .NET application? Whether you're building an ASP.NET Core web app, a background service, or a console tool, integrating a screenshot API is straightforward with C#'s async/await patterns and HttpClient.
This guide walks through everything from a basic one-liner to production-ready patterns including retry logic, caching, and ASP.NET Core integration. All examples use .NET 8+ and the GrabShot screenshot API.
You could spin up Playwright or Selenium in your .NET app, but there are real downsides:
A screenshot API handles all of this server-side. You send a URL, you get an image back. The rendering infrastructure is someone else's concern.
The simplest possible example using HttpClient:
using var client = new HttpClient();
var response = await client.GetAsync(
"https://api.grabshot.dev/v1/screenshot" +
"?url=https://example.com" +
"&width=1280&height=800" +
"&api_key=YOUR_API_KEY"
);
var imageBytes = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync("screenshot.png", imageBytes);
That's it. Three lines of meaningful code. You can grab a free API key to test this right now.
Before diving into C#, you might want to verify the API works from your terminal:
curl "https://api.grabshot.dev/v1/screenshot?url=https://example.com&width=1280&height=800&api_key=YOUR_API_KEY" \
--output screenshot.png
In a real application, you want proper error handling, typed responses, and IHttpClientFactory for connection pooling. Here's a clean service class:
public class ScreenshotService
{
private readonly HttpClient _client;
private readonly string _apiKey;
public ScreenshotService(HttpClient client, IConfiguration config)
{
_client = client;
_client.BaseAddress = new Uri("https://api.grabshot.dev");
_apiKey = config["GrabShot:ApiKey"]
?? throw new InvalidOperationException("GrabShot API key not configured");
}
public async Task<byte[]> CaptureAsync(
string url,
int width = 1280,
int height = 800,
string format = "png",
bool fullPage = false,
CancellationToken ct = default)
{
var query = $"/v1/screenshot?url={Uri.EscapeDataString(url)}" +
$"&width={width}&height={height}" +
$"&format={format}" +
$"&full_page={fullPage.ToString().ToLower()}" +
$"&api_key={_apiKey}";
var response = await _client.GetAsync(query, ct);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(ct);
throw new HttpRequestException(
$"Screenshot failed ({response.StatusCode}): {body}");
}
return await response.Content.ReadAsByteArrayAsync(ct);
}
}
Wire it up in Program.cs using the typed client pattern:
builder.Services.AddHttpClient<ScreenshotService>(client =>
{
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddStandardResilienceHandler(); // Polly retry + circuit breaker (.NET 8+)
The AddStandardResilienceHandler() from Microsoft.Extensions.Http.Resilience gives you automatic retries with exponential backoff and a circuit breaker, which is exactly what you want when calling external APIs.
A minimal API endpoint that captures a screenshot and returns it:
app.MapGet("/api/preview", async (
string url,
ScreenshotService screenshots,
CancellationToken ct) =>
{
var image = await screenshots.CaptureAsync(url, ct: ct);
return Results.File(image, "image/png", "preview.png");
});
Or if you want to cache screenshots in blob storage to avoid repeated API calls:
app.MapGet("/api/preview", async (
string url,
ScreenshotService screenshots,
BlobServiceClient blobs,
CancellationToken ct) =>
{
var container = blobs.GetBlobContainerClient("screenshots");
var blobName = $"{Convert.ToHexString(SHA256.HashData(
Encoding.UTF8.GetBytes(url)))}.png";
var blob = container.GetBlobClient(blobName);
if (await blob.ExistsAsync(ct))
{
var download = await blob.DownloadContentAsync(ct);
return Results.File(
download.Value.Content.ToArray(), "image/png");
}
var image = await screenshots.CaptureAsync(url, ct: ct);
await blob.UploadAsync(new BinaryData(image), ct);
return Results.File(image, "image/png", "preview.png");
});
Need to capture screenshots on a schedule? Use a hosted service. This is useful for monitoring dashboards, generating daily reports, or archiving competitor pages:
public class ScheduledScreenshotWorker : BackgroundService
{
private readonly ScreenshotService _screenshots;
private readonly ILogger<ScheduledScreenshotWorker> _logger;
private readonly string[] _urls;
public ScheduledScreenshotWorker(
ScreenshotService screenshots,
ILogger<ScheduledScreenshotWorker> logger,
IConfiguration config)
{
_screenshots = screenshots;
_logger = logger;
_urls = config.GetSection("MonitorUrls").Get<string[]>() ?? [];
}
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromHours(6));
while (await timer.WaitForNextTickAsync(ct))
{
foreach (var url in _urls)
{
try
{
var image = await _screenshots.CaptureAsync(url, ct: ct);
var filename = $"screenshots/{DateTime.UtcNow:yyyy-MM-dd}/{
Uri.EscapeDataString(url)}.png";
Directory.CreateDirectory(Path.GetDirectoryName(filename)!);
await File.WriteAllBytesAsync(filename, image, ct);
_logger.LogInformation("Captured {Url}", url);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to capture {Url}", url);
}
}
}
}
}
If your team also uses Python, here's the same basic capture:
import requests
response = requests.get("https://api.grabshot.dev/v1/screenshot", params={
"url": "https://example.com",
"width": 1280,
"height": 800,
"api_key": "YOUR_API_KEY"
})
with open("screenshot.png", "wb") as f:
f.write(response.content)
const response = await fetch(
`https://api.grabshot.dev/v1/screenshot?` +
`url=${encodeURIComponent('https://example.com')}` +
`&width=1280&height=800&api_key=YOUR_API_KEY`
);
const buffer = Buffer.from(await response.arrayBuffer());
await fs.promises.writeFile('screenshot.png', buffer);
25 free screenshots per month. No credit card required. Full API access including full-page capture, custom viewports, and PDF export.
Get Your API Key| Parameter | Type | Description |
|---|---|---|
| url | string | Target URL to screenshot (required) |
| width | int | Viewport width in pixels (default: 1280) |
| height | int | Viewport height in pixels (default: 800) |
| format | string | Output format: png, jpeg, webp |
| full_page | bool | Capture entire scrollable page |
| delay | int | Wait N ms before capture (for JS-heavy pages) |
| device | string | Emulate device (iphone14, pixel7, etc.) |
Screenshot APIs involve network I/O and rendering time. Pass CancellationToken through every async call so requests can be cancelled cleanly when a user navigates away or the app shuts down.
A typical screenshot takes 2-5 seconds. Set your HttpClient.Timeout to 30 seconds to handle slow-loading pages without hanging forever.
If the same URL is requested multiple times, cache the result. Use an in-memory cache for short-lived results or blob storage for longer persistence. A hash of the URL + parameters makes a good cache key.
Use IHttpClientFactory or the typed client pattern shown above. Creating new HttpClient() per request leads to socket exhaustion.
Integrating a website screenshot API into C# is clean and idiomatic. The async patterns in .NET 8 make it straightforward to build everything from simple capture tools to full production services with caching, retries, and scheduled jobs.
The GrabShot API documentation covers all available parameters and response formats. If you want to test endpoints interactively, the API playground lets you experiment without writing any code.