Calling SnapAPI from C# with HttpClient
The .NET HttpClient provides a clean async/await interface for making HTTP requests to SnapAPI. Register HttpClient as a singleton or through IHttpClientFactory in your dependency injection container to benefit from connection pooling. SnapAPI accepts Bearer token authentication via the Authorization header and returns binary image bytes or JSON responses, both of which HttpClient handles cleanly. The examples below use .NET 8 and C# 12 features including primary constructors and collection expressions, and are fully compatible with .NET 6 and 7 with minor syntax adjustments.
// SnapApiClient.cs — .NET HttpClient wrapper
using System.Net.Http.Headers;
public class SnapApiClient(HttpClient httpClient, IConfiguration config)
{
private readonly string _apiKey = config["SnapApi:Key"]
?? throw new InvalidOperationException("SnapApi:Key not configured");
public async Task<byte[]> ScreenshotAsync(
string url,
string format = "png",
bool fullPage = false,
int viewportWidth = 1280,
CancellationToken ct = default)
{
var qs = new Dictionary<string, string>
{
["url"] = url,
["format"] = format,
["full_page"] = fullPage.ToString().ToLower(),
["viewport_width"] = viewportWidth.ToString(),
};
var uri = QueryHelpers.AddQueryString("https://api.snapapi.pics/screenshot", qs);
using var req = new HttpRequestMessage(HttpMethod.Get, uri);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
var resp = await httpClient.SendAsync(req, ct);
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadAsByteArrayAsync(ct);
}
public async Task<string> ScrapeAsync(string url, CancellationToken ct = default)
{
var uri = QueryHelpers.AddQueryString("https://api.snapapi.pics/scrape",
new Dictionary<string, string> { ["url"] = url });
using var req = new HttpRequestMessage(HttpMethod.Get, uri);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
var resp = await httpClient.SendAsync(req, ct);
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadAsStringAsync(ct);
}
}
// Program.cs — register SnapApiClient with IHttpClientFactory
builder.Services.AddHttpClient<SnapApiClient>(client =>
{
client.Timeout = TimeSpan.FromSeconds(60);
});
builder.Configuration.AddEnvironmentVariables();
ASP.NET Core Controller: Screenshot Proxy
An ASP.NET Core minimal API or MVC controller can proxy screenshot requests to SnapAPI, protecting the API key from client exposure and adding authentication, rate limiting, and caching. The controller calls SnapAPI with the requested URL, receives the image bytes, and returns them as a FileContentResult with the appropriate MIME type. Add response caching middleware to cache frequently-requested screenshot URLs at the application level.
// ScreenshotController.cs — ASP.NET Core MVC controller
[ApiController]
[Route("api/[controller]")]
public class ScreenshotController(SnapApiClient snapApi) : ControllerBase
{
[HttpGet]
[ResponseCache(Duration = 3600, VaryByQueryKeys = ["url"])]
public async Task<IActionResult> Get(
[FromQuery] string url,
[FromQuery] string format = "png",
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(url))
return BadRequest("url is required");
try
{
var bytes = await snapApi.ScreenshotAsync(url, format, ct: ct);
var contentType = format switch
{
"jpeg" => "image/jpeg",
"webp" => "image/webp",
"pdf" => "application/pdf",
_ => "image/png"
};
return File(bytes, contentType);
}
catch (HttpRequestException ex)
{
return StatusCode(502, $"Screenshot failed: {ex.Message}");
}
}
}
Concurrent Screenshots with Task.WhenAll
C# async/await and Task.WhenAll make concurrent screenshot capture straightforward. Launch multiple screenshot tasks simultaneously and await all of them in parallel. This captures N pages in the time of the slowest single capture rather than the sum of all captures. Use SemaphoreSlim to limit concurrency to a configurable maximum to avoid overwhelming the API with too many simultaneous requests.
public class BatchScreenshotService(SnapApiClient client)
{
public async Task<Dictionary<string, byte[]>> CaptureAllAsync(
IEnumerable<string> urls,
int maxConcurrency = 10,
CancellationToken ct = default)
{
var semaphore = new SemaphoreSlim(maxConcurrency);
var results = new System.Collections.Concurrent.ConcurrentDictionary<string, byte[]>();
var tasks = urls.Select(async url =>
{
await semaphore.WaitAsync(ct);
try
{
var bytes = await client.ScreenshotAsync(url, ct: ct);
results.TryAdd(url, bytes);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed {url}: {ex.Message}");
}
finally
{
semaphore.Release();
}
});
await Task.WhenAll(tasks);
return new Dictionary<string, byte[]>(results);
}
}
Azure Functions: Serverless Screenshot Generation
Azure Functions with C# are a natural deployment target for screenshot generation tasks that run on a schedule or in response to events. A timer-triggered Azure Function can run a screenshot monitoring job every 30 minutes, capturing screenshots of monitored URLs and storing them in Azure Blob Storage. An HTTP-triggered function can serve as a screenshot proxy endpoint. Both function types share the same SnapApiClient implementation registered in the function app's dependency injection container.
// ScreenshotTimerFunction.cs — scheduled screenshot monitoring
public class ScreenshotTimerFunction(SnapApiClient client, BlobServiceClient blob)
{
[Function("ScreenshotMonitor")]
public async Task Run(
[TimerTrigger("0 */30 * * * *")] TimerInfo timer,
CancellationToken ct)
{
var urls = new[] { "https://example.com", "https://status.example.com" };
var container = blob.GetBlobContainerClient("screenshots");
await container.CreateIfNotExistsAsync(cancellationToken: ct);
foreach (var url in urls)
{
try
{
var bytes = await client.ScreenshotAsync(url, "png", fullPage: true, ct: ct);
var blobName = $"{Uri.EscapeDataString(url)}/{DateTime.UtcNow:yyyyMMddHHmmss}.png";
await container.UploadBlobAsync(blobName,
new BinaryData(bytes), ct);
Console.WriteLine($"Captured {url}: {bytes.Length} bytes");
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed {url}: {ex.Message}");
}
}
}
}
Configuration and Secret Management in .NET
Store the SnapAPI key in .NET user secrets during development and in Azure Key Vault, AWS Secrets Manager, or environment variables in production. Access it through IConfiguration with a key like SnapApi:Key, which maps to the SNAPAPI__KEY environment variable name using .NET's double-underscore environment variable separator convention. For Azure deployments, use the Azure Key Vault configuration provider to load the key from a Key Vault secret at application startup, keeping the API key out of application settings files and CI/CD pipeline logs. For Docker deployments, inject the key as a Docker secret or environment variable in the container definition. This approach follows the standard .NET configuration provider pattern and works with all deployment targets without code changes, since the SnapApiClient reads from IConfiguration regardless of where the value was loaded from.