ScreenshotNeo

BlogHow-to

How to Use a Screenshot API with C# and .NET

Call a hosted screenshot API from .NET with HttpClient: configure credentials, send capture options, handle image bytes and errors, and save the result.

By the ScreenshotNeo team4 October 202610 min read

A hosted screenshot API lets a .NET application turn a remote webpage URL into an image or PDF by sending an HTTP request. The practical pattern is to keep the API key on the server, send the URL and supported capture options, check the response and content type, then save or return the response body. Exact endpoints, authentication, option names, and response formats vary by provider.

The examples below use ScreenshotAPI.to’s documented GET endpoint, which returns screenshot bytes and authenticates with an x-api-key header. Use the endpoint and contract documented by your chosen provider; do not copy one service’s credentials or parameter names to another. ScreenshotAPI.to C# documentation.

1. Choose the right kind of screenshot

This guide covers a hosted service rendering a webpage from a URL. It is different from capturing the current screen of an app running on a device. For example, .NET MAUI’s Screenshot.CaptureAsync() captures the current screen of the running app; it does not render an arbitrary remote URL. See Microsoft’s MAUI API reference.

Before coding, confirm the provider’s endpoint, HTTP method, authentication, query or JSON field names, response type, supported formats and limits. Some APIs return the image bytes directly; others return JSON or redirect to a file. ScreenshotAPI.to documents a direct image response, while Screenshot API documents GET and POST requests and describes JSON and redirect response behavior in its REST documentation.

2. Make a screenshot request from C#

This complete .NET 6+ console example reads a key from an environment variable, URL-encodes the target URL, sends the documented x-api-key header, validates status and content type, and writes the returned bytes. Create a console project with dotnet new console, replace Program.cs, set the environment variable, and run dotnet run.

using System.Net.Http;

const string endpoint = "https://screenshotapi.to/api/v1/screenshot";
var apiKey = Environment.GetEnvironmentVariable("SCREENSHOTAPI_KEY");
if (string.IsNullOrWhiteSpace(apiKey))
    throw new InvalidOperationException("Set the SCREENSHOTAPI_KEY environment variable.");

var targetUrl = args.Length > 0 ? args[0] : "https://example.com";
var requestUri = $"{endpoint}?url={Uri.EscapeDataString(targetUrl)}";

using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(90) };
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.Add("x-api-key", apiKey);

using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
var body = await response.Content.ReadAsByteArrayAsync();
if (!response.IsSuccessStatusCode)
{
    var errorText = System.Text.Encoding.UTF8.GetString(body);
    throw new HttpRequestException(
        $"Screenshot request failed with {(int)response.StatusCode} {response.ReasonPhrase}: {errorText}");
}

var contentType = response.Content.Headers.ContentType?.MediaType;
if (contentType is null || !contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
    throw new InvalidDataException($"Expected image bytes, received '{contentType ?? "no content type"}'.");

var extension = contentType switch
{
    "image/jpeg" => ".jpg",
    "image/webp" => ".webp",
    _ => ".png"
};
var outputPath = "screenshot" + extension;
await File.WriteAllBytesAsync(outputPath, body);
Console.WriteLine($"Saved {body.Length} bytes to {outputPath}");

Set the key without putting it in source control. In Bash, for a one-off local run, use export SCREENSHOTAPI_KEY='your-key'; in PowerShell use $env:SCREENSHOTAPI_KEY='your-key'. In hosted ASP.NET applications, use the platform’s secret or environment configuration. Keep the key server-side: browser-delivered code exposes it, and query-string credentials can end up in logs or copied URLs. Screenshot API’s REST docs recommend header authentication over its query-key convenience option.

3. Add capture options carefully

Capture parameters control what the browser renders and when the capture happens. The available set and exact spelling depend on the provider. Check its current reference before adding a parameter; unsupported settings may be ignored or rejected.

Need Common control What to verify
Page shape Viewport width and height; full-page mode Whether full-page capture includes lazy-loaded content and whether very tall pages have a maximum height.
Output PNG, JPEG, WebP, or PDF; quality Whether the endpoint returns binary bytes, a redirect, or JSON with a URL; match the file extension to the actual format.
Device appearance Device preset, device scale factor, mobile emulation Preset values, viewport, user-agent behavior, and whether retina scale increases output dimensions.
Capture region CSS selector or clipping rectangle Behavior when the selector does not exist or matches multiple elements.
Timing Wait condition, selector wait, delay Whether the timeout is a render budget, network timeout, or both.
Page state Cookies, headers, user agent, timezone, geolocation Which values are sent to the remote browser and whether sensitive values are stored or logged.
Page modification Custom CSS or JavaScript; hide or click selectors Whether custom scripts execute before capture and what happens when a selector is missing.

For a GET API, encode every query parameter rather than concatenating untrusted strings. For complex options or sensitive values, use a provider’s documented POST body if available. Be especially careful with headers and cookies: they can grant access to private pages. Avoid logging them.

4. Handle the response in ASP.NET Core

For a web application, reuse managed HttpClient instances through IHttpClientFactory rather than creating a new client for every incoming request. Keep the key in server configuration and return an appropriate content type. This minimal controller action assumes the same binary endpoint and environment key as the console example:

using Microsoft.AspNetCore.Mvc;
using System.Net.Http.Headers;

[ApiController]
[Route("api/screenshots")]
public sealed class ScreenshotsController : ControllerBase
{
    private readonly IHttpClientFactory _clients;
    private readonly IConfiguration _configuration;

    public ScreenshotsController(IHttpClientFactory clients, IConfiguration configuration)
    {
        _clients = clients;
        _configuration = configuration;
    }

    [HttpGet]
    public async Task<IActionResult> Capture([FromQuery] string url, CancellationToken cancellationToken)
    {
        if (!Uri.TryCreate(url, UriKind.Absolute, out var target) ||
            (target.Scheme != Uri.UriSchemeHttp && target.Scheme != Uri.UriSchemeHttps))
            return BadRequest("url must be an absolute HTTP or HTTPS URL");

        var key = _configuration["SCREENSHOTAPI_KEY"];
        if (string.IsNullOrWhiteSpace(key))
            return Problem("Screenshot provider credentials are not configured.", statusCode: 500);

        var endpoint = "https://screenshotapi.to/api/v1/screenshot?url=" +
                       Uri.EscapeDataString(target.ToString());
        using var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
        request.Headers.Add("x-api-key", key);
        using var response = await _clients.CreateClient("screenshot").SendAsync(
            request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
        if (!response.IsSuccessStatusCode)
            return StatusCode((int)response.StatusCode, "Screenshot provider request failed.");

        var mediaType = response.Content.Headers.ContentType?.MediaType;
        if (mediaType is null || !mediaType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
            return Problem("Provider returned an unexpected response type.", statusCode: 502);

        var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken);
        return File(bytes, mediaType);
    }
}

Register the named client in startup configuration with builder.Services.AddHttpClient("screenshot", client => client.Timeout = TimeSpan.FromSeconds(90));. Set SCREENSHOTAPI_KEY in your deployment environment or secret store. Validate and constrain user-supplied URLs in a public endpoint: a screenshot proxy can otherwise be abused to make requests to destinations your service can reach. Apply your application’s authorization, rate limits, allowed-host policy, and output-size limits.

5. Equivalent requests in cURL, Python, and Node.js

These examples use the same ScreenshotAPI.to contract. They demonstrate the transport only; production code should also inspect content type and handle the provider’s documented error body.

cURL

curl --fail-with-body \
  -H "x-api-key: $SCREENSHOTAPI_KEY" \
  --get "https://screenshotapi.to/api/v1/screenshot" \
  --data-urlencode "url=https://example.com" \
  --output screenshot.png

Python

import os
import requests

response = requests.get(
    "https://screenshotapi.to/api/v1/screenshot",
    headers={"x-api-key": os.environ["SCREENSHOTAPI_KEY"]},
    params={"url": "https://example.com"},
    timeout=90,
)
response.raise_for_status()
content_type = response.headers.get("Content-Type", "").split(";", 1)[0]
if not content_type.startswith("image/"):
    raise ValueError(f"Expected image bytes, got {content_type!r}")
with open("screenshot.png", "wb") as output:
    output.write(response.content)

Node.js

const target = new URL("https://example.com");
const endpoint = new URL("https://screenshotapi.to/api/v1/screenshot");
endpoint.searchParams.set("url", target.toString());

const response = await fetch(endpoint, {
  headers: { "x-api-key": process.env.SCREENSHOTAPI_KEY },
  signal: AbortSignal.timeout(90_000),
});
if (!response.ok) {
  throw new Error(`Screenshot request failed: ${response.status} ${await response.text()}`);
}
const contentType = response.headers.get("content-type") ?? "";
if (!contentType.startsWith("image/")) {
  throw new Error(`Expected image bytes, got ${contentType}`);
}
const bytes = Buffer.from(await response.arrayBuffer());
await import("node:fs/promises").then(({ writeFile }) => writeFile("screenshot.png", bytes));

6. Or skip the browser setup

ScreenshotNeo is a hosted screenshot API and MCP server. Its API takes a URL and returns PNG, JPEG, WebP, or PDF. The request below uses its documented access key and URL parameters; see the ScreenshotNeo API documentation for options and response details.

using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(90) };
var requestUrl = "https://api.screenshotneo.com/v1/shot?" +
    "access_key=" + Uri.EscapeDataString(Environment.GetEnvironmentVariable("SCREENSHOTNEO_API_KEY")!) +
    "&url=" + Uri.EscapeDataString("https://example.com");
using var response = await client.GetAsync(requestUrl);
response.EnsureSuccessStatusCode();
var bytes = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync("shot.webp", bytes);

Or use the documented command-line request:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo accepts cookie and consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots.

Sign up for 1,000 free screenshots a month, with no card required.

7. Troubleshooting

Symptom Likely cause What to do
401 or 403 Missing, invalid, expired, or incorrectly placed API key. Check the provider’s exact auth scheme and header name; confirm the deployed secret is set. Do not assume another provider’s header works.
400 or validation error Malformed target URL, unsupported option, incorrect parameter name, or wrong HTTP method. URL-encode the target; compare each field and method against the provider’s current API reference.
429 or quota response Rate limit or plan quota reached. Follow the provider’s retry guidance; honor any retry headers, reduce concurrency, and inspect quota before retrying repeatedly.
200 response but file is not an image The API returned JSON, HTML, or a redirect response rather than binary image content. Check the content type and endpoint mode. Parse the documented JSON response or follow its documented redirect instead of saving it with an image extension.
Screenshot is blank or incomplete The page needs more rendering time, authentication, client-side data, or scrolling to load lazy images. Use a supported wait condition or selector, supply required cookies or headers securely, and enable full-page/lazy-load behavior if the service offers it.
Selector capture fails The target element is absent, late to render, or selector syntax is invalid. Verify the selector on the rendered page, wait for it when supported, and handle missing-selector responses explicitly.
Timeout or connection exception Slow target site, provider render deadline, network issue, or client deadline. Distinguish the provider’s render timeout from your HTTP timeout; allow a suitable bounded deadline and retry only transient failures.
Saved file has wrong extension Extension was hard-coded while requesting another format or receiving another content type. Choose the extension from the requested format and validate the response content type before saving.

Status codes and error payloads are provider-specific. Screenshot API’s REST reference documents examples including unauthorized, invalid request, rate limit or quota, render failure, and missing selector responses. Treat those as that provider’s contract, not universal guarantees.

8. Performance, reliability, and cost

  • Reuse HTTP clients. In ASP.NET Core, use IHttpClientFactory or another managed client lifecycle. Avoid constructing a client per request.
  • Bound work. Set a transport timeout and pass cancellation tokens from incoming requests. Limit concurrency and response size so many large full-page captures cannot exhaust resources.
  • Choose capture settings deliberately. Full-page output, high device scale, large viewports, delays, and network-idle waits can increase render time and image size. Use the smallest dimensions and wait condition that meet the requirement.
  • Retry selectively. A retry may help with transient network failures or throttling when the provider says it is safe. Avoid immediate repeated retries for invalid input, bad credentials, missing selectors, or known render failures.
  • Cache when appropriate. If the page can be reused, consider provider caching or application caching, while accounting for freshness and personalized content. Never share a cached response across users when cookies or authorization make the page user-specific.
  • Budget from actual usage. Compare documented plan limits, overage behavior, concurrency, retention, and response mode for each service. The research available here does not establish independent price, speed, uptime, privacy, or quality comparisons among third-party providers.

9. FAQ

Do I need a browser package in my .NET application?

Not when you call a hosted screenshot API: your application sends an HTTP request. Running a browser locally is a separate approach with browser installation, process management, and deployment requirements.

Can I return the image without writing a file?

Yes. In ASP.NET Core, return the validated bytes with the actual media type, as the controller example does. For a console or worker, pass the byte array to the next processing step instead of saving it.

Can I capture a webpage that requires login?

Only if the provider supports the necessary session cookies or authorization headers and you are authorized to access the page. Send credentials only over the documented secure mechanism and avoid exposing them in logs or generated public URLs.

Is .NET MAUI’s Screenshot API the same thing?

No. MAUI captures the current displayed screen of the running application. A hosted screenshot API renders a URL in a remote browser and returns the result.

Should I use a .NET SDK or HttpClient?

HttpClient avoids an extra package and makes the HTTP contract explicit. A provider SDK can reduce request and response plumbing, but check its target .NET version, package maintenance, exception model, and supported options. For example, Screenshot Scout’s repository documents a .NET 8+ SDK, while ScreenshotAPI.to documents direct HttpClient use and no official .NET SDK: Screenshot Scout .NET SDK.