ScreenshotNeo

BlogGuides

Screenshot API for C#: Quick Start and Examples

Call a screenshot API from .NET 6+ with HttpClient, save image bytes, add full-page options, handle errors, and integrate with ASP.NET.

By the ScreenshotNeo team1 October 20269 min read

Direct answer: In .NET 6 or later, call a screenshot API with the built-in HttpClient. Keep the API key in an environment variable, send it in the x-api-key header, URL-encode the target page, validate the response, then write the returned bytes to a file.

1. Prerequisites

  • .NET 6, 7, 8, 9 or later.
  • An API key from your screenshot provider.
  • A target URL that your provider can access.

The examples below use the ScreenshotAPI.to REST endpoint documented for C#. That integration does not require an external .NET SDK or NuGet package.

2. Minimal C# console example

Create a project and store your key outside source control:

dotnet new console -n ScreenshotDemo
cd ScreenshotDemo
# macOS/Linux
export SCREENSHOTAPI_KEY='your-api-key'
# Windows PowerShell
$env:SCREENSHOTAPI_KEY = 'your-api-key'

Replace Program.cs with:

using System.Web;

var apiKey = Environment.GetEnvironmentVariable("SCREENSHOTAPI_KEY")
             ?? throw new InvalidOperationException("Missing SCREENSHOTAPI_KEY");

using var client = new HttpClient
{
    Timeout = TimeSpan.FromSeconds(90)
};
client.DefaultRequestHeaders.Add("x-api-key", apiKey);

var query = HttpUtility.ParseQueryString(string.Empty);
query["url"] = "https://example.com";

using var response = await client.GetAsync(
    $"https://screenshotapi.to/api/v1/screenshot?{query}");

if (!response.IsSuccessStatusCode)
{
    var error = await response.Content.ReadAsStringAsync();
    throw new HttpRequestException(
        $"Screenshot request failed ({(int)response.StatusCode} {response.ReasonPhrase}): {error}");
}

var bytes = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync("screenshot.png", bytes);

Console.WriteLine($"Saved {bytes.Length:N0} bytes to screenshot.png");

Run it with dotnet run. The response is binary image data, so do not parse it as JSON unless you explicitly selected a response mode that returns metadata or a URL.

3. A reusable ScreenshotAPI client

A small wrapper keeps request construction, error handling and response headers in one place. The result exposes the content type, remaining credits, screenshot ID and render duration when the service sends those headers.

using System.Net.Http.Headers;
using System.Web;

public sealed record ScreenshotOptions(
    string Url,
    int? Width = null,
    int? Height = null,
    bool FullPage = false,
    string Format = "png",
    int? Quality = null,
    string? ColorScheme = null,
    string? WaitUntil = null,
    string? WaitForSelector = null,
    int? Delay = null);

public sealed record ScreenshotResult(
    byte[] Content,
    string ContentType,
    string? CreditsRemaining,
    string? ScreenshotId,
    string? DurationMs);

public sealed class ScreenshotApi
{
    private readonly HttpClient _http;

    public ScreenshotApi(HttpClient httpClient, string apiKey)
    {
        _http = httpClient;
        if (!_http.DefaultRequestHeaders.Contains("x-api-key"))
            _http.DefaultRequestHeaders.Add("x-api-key", apiKey);
    }

    public async Task CaptureAsync(
        ScreenshotOptions options,
        CancellationToken cancellationToken = default)
    {
        if (!Uri.TryCreate(options.Url, UriKind.Absolute, out var target) ||
            (target.Scheme != Uri.UriSchemeHttp && target.Scheme != Uri.UriSchemeHttps))
            throw new ArgumentException("Url must be an absolute HTTP or HTTPS URL", nameof(options));

        var query = HttpUtility.ParseQueryString(string.Empty);
        query["url"] = options.Url;
        if (options.Width is not null) query["width"] = options.Width.Value.ToString();
        if (options.Height is not null) query["height"] = options.Height.Value.ToString();
        if (options.FullPage) query["full_page"] = "true";
        if (!string.IsNullOrWhiteSpace(options.Format)) query["format"] = options.Format;
        if (options.Quality is not null) query["quality"] = options.Quality.Value.ToString();
        if (!string.IsNullOrWhiteSpace(options.ColorScheme)) query["color_scheme"] = options.ColorScheme;
        if (!string.IsNullOrWhiteSpace(options.WaitUntil)) query["wait_until"] = options.WaitUntil;
        if (!string.IsNullOrWhiteSpace(options.WaitForSelector)) query["wait_for_selector"] = options.WaitForSelector;
        if (options.Delay is not null) query["delay"] = options.Delay.Value.ToString();

        using var response = await _http.GetAsync(
            $"https://screenshotapi.to/api/v1/screenshot?{query}",
            HttpCompletionOption.ResponseHeadersRead,
            cancellationToken);

        if (!response.IsSuccessStatusCode)
        {
            var error = await response.Content.ReadAsStringAsync(cancellationToken);
            throw new HttpRequestException(
                $"Screenshot API returned {(int)response.StatusCode}: {error}",
                null,
                response.StatusCode);
        }

        return new ScreenshotResult(
            await response.Content.ReadAsByteArrayAsync(cancellationToken),
            response.Content.Headers.ContentType?.MediaType ?? "application/octet-stream",
            Header(response, "x-credits-remaining"),
            Header(response, "x-screenshot-id"),
            Header(response, "x-duration-ms"));
    }

    private static string? Header(HttpResponseMessage response, string name) =>
        response.Headers.TryGetValues(name, out var values) ? values.FirstOrDefault() : null;
}

Example usage:

using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(90) };
var key = Environment.GetEnvironmentVariable("SCREENSHOTAPI_KEY")
          ?? throw new InvalidOperationException("Missing API key");
var api = new ScreenshotApi(http, key);

var result = await api.CaptureAsync(new ScreenshotOptions(
    Url: "https://example.com/pricing",
    Width: 1440,
    FullPage: true,
    Format: "webp",
    Quality: 85,
    WaitUntil: "networkidle"));

await File.WriteAllBytesAsync("pricing.webp", result.Content);
Console.WriteLine($"{result.ContentType}, {result.DurationMs} ms, credits left: {result.CreditsRemaining}");

4. Capture options

Option Purpose Typical use
width, height Set the viewport dimensions. Desktop, tablet or mobile layouts.
full_page=true Capture the full scrollable document. Long articles and invoices.
format Select png, jpeg or webp. PNG for lossless output; WebP for smaller files.
quality Set lossy image quality where supported. Use a value such as 85 for WebP.
color_scheme Request light or dark rendering. Visual regression checks for both themes.
wait_until Choose a page readiness condition. Wait for network idle on JavaScript-heavy pages.
wait_for_selector Wait until a CSS selector exists. Capture after a chart or dashboard mounts.
delay Add a fixed delay before capture. Animations or late-loading embeds.

The REST reference also documents POST capture requests for complex JSON configurations, batch capture, selector or element capture, custom CSS and JavaScript, geolocation, timezone, ad and cookie blocking, PDF output and other rendering controls. Use POST when query strings become difficult to maintain. Confirm the response mode for your account: the reference describes JSON and redirect workflows, while the basic C# example receives image bytes directly.

5. Full-page, WebP and multiple URLs

Full-page PNG

var result = await api.CaptureAsync(new ScreenshotOptions(
    "https://example.com/docs",
    FullPage: true));
await File.WriteAllBytesAsync("docs.png", result.Content);

WebP output

var result = await api.CaptureAsync(new ScreenshotOptions(
    "https://example.com",
    Format: "webp",
    Quality: 85));
await File.WriteAllBytesAsync("home.webp", result.Content);

Concurrent captures

var urls = new[]
{
    "https://example.com",
    "https://example.com/about",
    "https://example.com/contact"
};

var tasks = urls.Select(async (url, index) =>
{
    try
    {
        var image = await api.CaptureAsync(new ScreenshotOptions(url));
        await File.WriteAllBytesAsync($"screenshot-{index}.png", image.Content);
        return (url, Error: (string?)null);
    }
    catch (Exception ex)
    {
        return (url, Error: ex.Message);
    }
}).ToArray();

var results = await Task.WhenAll(tasks);
foreach (var item in results)
    Console.WriteLine(item.Error is null ? $"OK {item.url}" : $"FAILED {item.url}: {item.Error}");

Bound concurrency with a SemaphoreSlim when processing large URL lists. Sending one task per URL without a limit can trigger rate limiting and increase memory use.

6. cURL, Python and Node.js equivalents

cURL

curl -G "https://screenshotapi.to/api/v1/screenshot" \
  -H "x-api-key: $SCREENSHOTAPI_KEY" \
  --data-urlencode "url=https://example.com" \
  --data "full_page=true" \
  -o screenshot.png

Python

import os
import requests

key = os.environ["SCREENSHOTAPI_KEY"]
r = requests.get(
    "https://screenshotapi.to/api/v1/screenshot",
    headers={"x-api-key": key},
    params={"url": "https://example.com", "full_page": "true"},
    timeout=90,
)
r.raise_for_status()
with open("screenshot.png", "wb") as f:
    f.write(r.content)

Node.js

const key = process.env.SCREENSHOTAPI_KEY;
const q = new URLSearchParams({
  url: 'https://example.com',
  full_page: 'true'
});
const res = await fetch(`https://screenshotapi.to/api/v1/screenshot?${q}`, {
  headers: { 'x-api-key': key },
  signal: AbortSignal.timeout(90_000)
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const body = Buffer.from(await res.arrayBuffer());
await require('node:fs').promises.writeFile('screenshot.png', body);

7. ASP.NET Core integration

Minimal API

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient<ScreenshotApi>(client =>
{
    client.Timeout = TimeSpan.FromSeconds(90);
});

var app = builder.Build();
app.MapGet("/screenshot", async (string url, ScreenshotApi api, CancellationToken ct) =>
{
    if (!Uri.TryCreate(url, UriKind.Absolute, out var parsed) ||
        (parsed.Scheme != Uri.UriSchemeHttp && parsed.Scheme != Uri.UriSchemeHttps))
        return Results.BadRequest("url must be an absolute HTTP or HTTPS URL");

    try
    {
        var result = await api.CaptureAsync(new ScreenshotOptions(url), ct);
        return Results.File(result.Content, result.ContentType);
    }
    catch (HttpRequestException ex)
    {
        return Results.Problem(ex.Message, statusCode: StatusCodes.Status502BadGateway);
    }
});
app.Run();

Register the API key through configuration or a secret store, then construct ScreenshotApi with the injected HttpClient. Do not accept unrestricted user URLs in a public endpoint without SSRF protections and an allowlist.

Controller response with caching

[ApiController]
[Route("api/screenshots")]
public sealed class ScreenshotsController : ControllerBase
{
    private readonly ScreenshotApi _api;
    public ScreenshotsController(ScreenshotApi api) => _api = api;

    [HttpGet]
    public async Task<IActionResult> Get(string url, CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(url)) return BadRequest("url is required");
        try
        {
            var result = await _api.CaptureAsync(new ScreenshotOptions(url), ct);
            Response.Headers.CacheControl = "public, max-age=3600";
            return File(result.Content, result.ContentType);
        }
        catch (HttpRequestException ex)
        {
            return StatusCode(502, ex.Message);
        }
    }
}

8. Errors and troubleshooting

Response Likely cause Fix
400 invalid_request Missing or malformed parameter. Validate the absolute URL and encode query values.
401 unauthorized Credentials were not accepted. Check the environment variable and header name.
402 Out of credits. Check remaining credits and plan limits.
403 Invalid or disallowed API key. Regenerate the key or verify account permissions.
422 selector_not_found The requested selector did not appear. Check the selector, increase the wait condition or remove the selector wait.
429 rate_limited or quota_exceeded Too many requests or monthly quota exhausted. Limit concurrency, retry with backoff and monitor quota headers.
502 render_failed The target page failed during rendering. Retry transient failures; inspect the URL, scripts and upstream availability.
Timeout Slow page, blocked resource or insufficient wait budget. Set a suitable client timeout, reduce unnecessary waits and retry selectively.
Downloaded file is JSON or HTML Error body was saved as if it were an image. Check IsSuccessStatusCode and content type before writing bytes.

9. Performance, reliability and cost

  • Reuse one HttpClient instead of creating one per request.
  • Use ResponseHeadersRead for large images so the response can stream sooner.
  • Use WebP or JPEG when smaller files matter; retain PNG for pixel-accurate diffs.
  • Set explicit waits only when needed. Network-idle and long fixed delays increase latency.
  • Retry 429 and transient 5xx responses with exponential backoff and jitter. Do not retry authentication or validation errors blindly.
  • Record status code, request URL, screenshot ID, duration and remaining credits. Never log the API key.
  • The documented free plan lists 60 requests per minute and 500 screenshots per month. Treat those as operational limits and read response headers for current values.
  • Batch endpoints are useful for many URLs; track each item and preserve partial failures rather than retrying an entire batch unnecessarily.

10. Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP or PDF. Cookie and consent banners are accepted and removed before capture, along with more than 60 known consent platforms, newsletter popups and chat widgets. Bot checks, blank pages, failed loads, timeouts and cache hits are not billed, and the response identifies the verdict and billing status in X-Page-Verdict and X-Billed headers.

Use the documented options and examples at ScreenshotNeo docs:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also supports full-page and element captures, dark mode, device presets, custom CSS and JavaScript, click and wait actions, blocked resources, headers, cookies, user agents, timezone, geolocation, resizing, caching, signed links, asynchronous jobs, webhooks, bulk capture and an MCP server for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

11. FAQ

Is there an official .NET SDK?

The documented C# route uses built-in HttpClient; no external package is required for these examples.

Should I use GET or POST?

GET is convenient for a URL and a few parameters. Use POST JSON for advanced rendering settings or batch work.

How do I know whether the response is an image?

Check the HTTP status first, then inspect Content-Type. Save bytes only after a successful response.

Can I expose this directly to browsers?

Keep the API key on your server. Have your ASP.NET endpoint call the provider and return the image.

How do I prevent users from abusing a URL parameter?

Allow only http and https, apply an allowlist where possible, block private network ranges and impose request and response size limits.