Website Screenshot API in C# and .NET

February 22, 2026 · 10 min read

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.

Why Use an API Instead of a Headless Browser?

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.

Quick Start: Your First Screenshot in C#

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.

Using curl (For Quick Testing)

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

Production-Ready Screenshot Service

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);
    }
}

Registering with Dependency Injection

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.

ASP.NET Core Controller Example

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");
});

Background Service for Scheduled Screenshots

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);
                }
            }
        }
    }
}

Python Equivalent (For Comparison)

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)

Node.js Equivalent

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);

Try GrabShot Free

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

API Parameters Reference

ParameterTypeDescription
urlstringTarget URL to screenshot (required)
widthintViewport width in pixels (default: 1280)
heightintViewport height in pixels (default: 800)
formatstringOutput format: png, jpeg, webp
full_pageboolCapture entire scrollable page
delayintWait N ms before capture (for JS-heavy pages)
devicestringEmulate device (iphone14, pixel7, etc.)

Common Patterns and Tips

1. Always Use CancellationToken

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.

2. Set Reasonable Timeouts

A typical screenshot takes 2-5 seconds. Set your HttpClient.Timeout to 30 seconds to handle slow-loading pages without hanging forever.

3. Cache Aggressively

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.

4. Don't Create HttpClient Instances Per Request

Use IHttpClientFactory or the typed client pattern shown above. Creating new HttpClient() per request leads to socket exhaustion.

Wrapping Up

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.