Screenshot API with C# and .NET: Complete Integration Guide

2026-05-11 | Tags: [screenshot-api, csharp, dotnet, azure, aspnet, blazor, csharp-tutorial]

C# and .NET developers tend to be thorough. They want strongly-typed models, proper async patterns, clean dependency injection, and code that fits naturally into the Microsoft ecosystem. This guide covers all of it — from a basic HttpClient call through to Azure Functions triggers and Blazor Server components.

Basic Setup: HttpClient with Typed Response

Start with a typed client wrapper. This handles the HTTP concerns cleanly and is easy to mock in tests.

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

public class ScreenshotOptions
{
    public string Url { get; set; } = string.Empty;
    public int Width { get; set; } = 1280;
    public int Height { get; set; } = 800;
    public int Delay { get; set; } = 0;
    public string Format { get; set; } = "png";
    public bool FullPage { get; set; } = false;
    public bool BlockAds { get; set; } = false;
    public string? Js { get; set; }
}

public class ScreenshotResult
{
    public bool Success { get; set; }
    public byte[]? ImageData { get; set; }
    public string? ContentType { get; set; }
    public string? ErrorMessage { get; set; }
}

public class ScreenshotApiClient
{
    private readonly HttpClient _httpClient;
    private readonly string _baseUrl = "https://hermesforge.dev/api/screenshot";

    public ScreenshotApiClient(HttpClient httpClient, string apiKey)
    {
        _httpClient = httpClient;
        _httpClient.DefaultRequestHeaders.Add("X-API-Key", apiKey);
    }

    public async Task<ScreenshotResult> CaptureAsync(
        ScreenshotOptions options,
        CancellationToken cancellationToken = default)
    {
        var queryParams = new Dictionary<string, string?>
        {
            ["url"] = options.Url,
            ["width"] = options.Width.ToString(),
            ["height"] = options.Height.ToString(),
            ["format"] = options.Format,
            ["full_page"] = options.FullPage.ToString().ToLower(),
            ["block_ads"] = options.BlockAds.ToString().ToLower(),
        };

        if (options.Delay > 0)
            queryParams["delay"] = options.Delay.ToString();
        if (!string.IsNullOrEmpty(options.Js))
            queryParams["js"] = options.Js;

        var qs = string.Join("&", queryParams
            .Where(kv => kv.Value != null)
            .Select(kv => $"{kv.Key}={Uri.EscapeDataString(kv.Value!)}"));

        var requestUrl = $"{_baseUrl}?{qs}";

        try
        {
            var response = await _httpClient.GetAsync(requestUrl, cancellationToken);

            if (!response.IsSuccessStatusCode)
            {
                return new ScreenshotResult
                {
                    Success = false,
                    ErrorMessage = $"API returned {(int)response.StatusCode}: {response.ReasonPhrase}"
                };
            }

            var imageBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken);
            var contentType = response.Content.Headers.ContentType?.MediaType ?? "image/png";

            return new ScreenshotResult
            {
                Success = true,
                ImageData = imageBytes,
                ContentType = contentType
            };
        }
        catch (HttpRequestException ex)
        {
            return new ScreenshotResult { Success = false, ErrorMessage = ex.Message };
        }
    }
}

Dependency Injection in ASP.NET Core

Register the client in Program.cs:

// Program.cs
builder.Services.AddHttpClient<ScreenshotApiClient>(client =>
{
    client.Timeout = TimeSpan.FromSeconds(30);
})
.AddTransientHttpErrorPolicy(policy =>
    policy.WaitAndRetryAsync(3, retryAttempt =>
        TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))));

builder.Services.AddScoped<ScreenshotApiClient>(provider =>
{
    var httpClient = provider.GetRequiredService<IHttpClientFactory>()
        .CreateClient(nameof(ScreenshotApiClient));
    var apiKey = builder.Configuration["ScreenshotApi:ApiKey"]
        ?? throw new InvalidOperationException("ScreenshotApi:ApiKey not configured");
    return new ScreenshotApiClient(httpClient, apiKey);
});

In appsettings.json:

{
  "ScreenshotApi": {
    "ApiKey": "your_api_key_here"
  }
}

Inject and use in a controller:

[ApiController]
[Route("api/[controller]")]
public class CaptureController : ControllerBase
{
    private readonly ScreenshotApiClient _screenshotClient;

    public CaptureController(ScreenshotApiClient screenshotClient)
    {
        _screenshotClient = screenshotClient;
    }

    [HttpGet("capture")]
    public async Task<IActionResult> Capture([FromQuery] string url, CancellationToken ct)
    {
        if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) ||
            (uri.Scheme != "http" && uri.Scheme != "https"))
        {
            return BadRequest("Invalid URL");
        }

        var result = await _screenshotClient.CaptureAsync(new ScreenshotOptions
        {
            Url = url,
            Width = 1280,
            Height = 800,
            Delay = 1000,
            Format = "webp",
        }, ct);

        if (!result.Success)
            return StatusCode(502, result.ErrorMessage);

        return File(result.ImageData!, result.ContentType!, "screenshot.webp");
    }
}

Azure Functions: HTTP Trigger

Azure Functions is popular in the Microsoft ecosystem for event-driven capture workflows.

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using System.Net;

public class ScreenshotFunction
{
    private readonly ScreenshotApiClient _screenshotClient;
    private readonly ILogger<ScreenshotFunction> _logger;

    public ScreenshotFunction(ScreenshotApiClient screenshotClient, ILogger<ScreenshotFunction> logger)
    {
        _screenshotClient = screenshotClient;
        _logger = logger;
    }

    [Function("CaptureScreenshot")]
    public async Task<HttpResponseData> RunAsync(
        [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req,
        FunctionContext context)
    {
        var body = await req.ReadFromJsonAsync<CaptureRequest>();
        if (body?.Url is null)
        {
            var badReq = req.CreateResponse(HttpStatusCode.BadRequest);
            await badReq.WriteStringAsync("Missing 'url' field");
            return badReq;
        }

        _logger.LogInformation("Capturing screenshot of {Url}", body.Url);

        var result = await _screenshotClient.CaptureAsync(new ScreenshotOptions
        {
            Url = body.Url,
            Width = body.Width ?? 1280,
            Height = body.Height ?? 800,
            Delay = body.Delay ?? 1000,
            Format = "png",
        });

        if (!result.Success)
        {
            _logger.LogWarning("Screenshot capture failed: {Error}", result.ErrorMessage);
            var errorResp = req.CreateResponse(HttpStatusCode.BadGateway);
            await errorResp.WriteStringAsync(result.ErrorMessage ?? "Capture failed");
            return errorResp;
        }

        var response = req.CreateResponse(HttpStatusCode.OK);
        response.Headers.Add("Content-Type", "image/png");
        await response.Body.WriteAsync(result.ImageData!);
        return response;
    }
}

public record CaptureRequest(string? Url, int? Width, int? Height, int? Delay);

Register in Program.cs for Azure Functions isolated worker:

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices((context, services) =>
    {
        services.AddHttpClient<ScreenshotApiClient>();
        services.AddSingleton(sp =>
        {
            var http = sp.GetRequiredService<IHttpClientFactory>().CreateClient();
            var key = context.Configuration["ScreenshotApi:ApiKey"]!;
            return new ScreenshotApiClient(http, key);
        });
    })
    .Build();

host.Run();

Azure Functions: Timer Trigger for Scheduled Monitoring

public class ScheduledMonitorFunction
{
    private readonly ScreenshotApiClient _client;
    private readonly ILogger<ScheduledMonitorFunction> _logger;

    // Pages to monitor: stored in Azure App Configuration or appsettings
    private static readonly string[] MonitorTargets =
    [
        "https://yoursite.com",
        "https://yoursite.com/pricing",
        "https://yoursite.com/product",
    ];

    public ScheduledMonitorFunction(ScreenshotApiClient client, ILogger<ScheduledMonitorFunction> logger)
    {
        _client = client;
        _logger = logger;
    }

    // Runs every 6 hours: "0 0 */6 * * *"
    [Function("ScheduledMonitor")]
    public async Task RunAsync([TimerTrigger("0 0 */6 * * *")] TimerInfo timer)
    {
        _logger.LogInformation("Starting scheduled visual monitor at {Time}", DateTimeOffset.UtcNow);

        var tasks = MonitorTargets.Select(url => CaptureAndStoreAsync(url));
        var results = await Task.WhenAll(tasks);

        var failed = results.Count(r => !r);
        _logger.LogInformation(
            "Monitor complete: {Success}/{Total} captures succeeded",
            results.Length - failed, results.Length);
    }

    private async Task<bool> CaptureAndStoreAsync(string url)
    {
        var result = await _client.CaptureAsync(new ScreenshotOptions
        {
            Url = url,
            Width = 1440,
            Height = 900,
            Delay = 2000,
            Format = "png",
        });

        if (!result.Success)
        {
            _logger.LogWarning("Failed to capture {Url}: {Error}", url, result.ErrorMessage);
            return false;
        }

        // Store to Azure Blob Storage (BlobClient omitted for brevity)
        var slug = new Uri(url).AbsolutePath.Trim('/').Replace('/', '_');
        var timestamp = DateTimeOffset.UtcNow.ToString("yyyyMMdd_HHmmss");
        var blobName = $"screenshots/{slug}/{timestamp}.png";

        _logger.LogInformation("Stored {Bytes} bytes for {Url} as {Blob}",
            result.ImageData!.Length, url, blobName);

        return true;
    }
}

Blazor Server: Display Live Screenshots

In a Blazor Server component, call the API and display the result inline:

@page "/preview"
@inject ScreenshotApiClient ScreenshotClient

<h3>Page Preview</h3>

<EditForm Model="@model" OnValidSubmit="@CaptureScreenshot">
    <InputText @bind-Value="model.Url" placeholder="https://example.com" class="form-control" />
    <button type="submit" class="btn btn-primary mt-2" disabled="@isLoading">
        @(isLoading ? "Capturing..." : "Capture")
    </button>
</EditForm>

@if (errorMessage is not null)
{
    <div class="alert alert-danger mt-3">@errorMessage</div>
}

@if (imageDataUri is not null)
{
    <div class="mt-3">
        <img src="@imageDataUri" class="img-fluid border rounded" alt="Screenshot" />
    </div>
}

@code {
    private CaptureModel model = new();
    private string? imageDataUri;
    private string? errorMessage;
    private bool isLoading;

    private async Task CaptureScreenshot()
    {
        isLoading = true;
        errorMessage = null;
        imageDataUri = null;

        try
        {
            var result = await ScreenshotClient.CaptureAsync(new ScreenshotOptions
            {
                Url = model.Url,
                Width = 1280,
                Height = 800,
                Delay = 1000,
                Format = "png",
            });

            if (result.Success)
            {
                var base64 = Convert.ToBase64String(result.ImageData!);
                imageDataUri = $"data:{result.ContentType};base64,{base64}";
            }
            else
            {
                errorMessage = result.ErrorMessage;
            }
        }
        finally
        {
            isLoading = false;
        }
    }

    private class CaptureModel
    {
        public string Url { get; set; } = string.Empty;
    }
}

Saving to Disk or Returning as File Download

// Save to disk
public static async Task SaveScreenshotAsync(ScreenshotResult result, string outputPath)
{
    if (!result.Success || result.ImageData is null)
        throw new InvalidOperationException(result.ErrorMessage);

    await File.WriteAllBytesAsync(outputPath, result.ImageData);
}

// Return as file download from MVC/API controller
public IActionResult DownloadScreenshot(ScreenshotResult result, string filename = "screenshot.png")
{
    if (!result.Success || result.ImageData is null)
        return BadRequest(result.ErrorMessage);

    return File(result.ImageData, result.ContentType ?? "image/png", filename);
}

// Convert to base64 string (for embedding in JSON responses or HTML)
public static string ToBase64DataUri(ScreenshotResult result)
{
    if (!result.Success || result.ImageData is null)
        throw new InvalidOperationException(result.ErrorMessage);

    var base64 = Convert.ToBase64String(result.ImageData);
    return $"data:{result.ContentType ?? "image/png"};base64,{base64}";
}

Unit Testing with Mocks

using Moq;
using Xunit;

public class CaptureControllerTests
{
    [Fact]
    public async Task Capture_ReturnsFile_WhenApiSucceeds()
    {
        // Arrange
        var mockClient = new Mock<ScreenshotApiClient>(
            new HttpClient(), "test-key");

        mockClient
            .Setup(c => c.CaptureAsync(It.IsAny<ScreenshotOptions>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(new ScreenshotResult
            {
                Success = true,
                ImageData = new byte[] { 0x89, 0x50, 0x4E, 0x47 }, // PNG header
                ContentType = "image/png"
            });

        var controller = new CaptureController(mockClient.Object);

        // Act
        var result = await controller.Capture("https://example.com", CancellationToken.None);

        // Assert
        var fileResult = Assert.IsType<FileContentResult>(result);
        Assert.Equal("image/png", fileResult.ContentType);
    }

    [Fact]
    public async Task Capture_ReturnsBadRequest_ForInvalidUrl()
    {
        var mockClient = new Mock<ScreenshotApiClient>(new HttpClient(), "test-key");
        var controller = new CaptureController(mockClient.Object);

        var result = await controller.Capture("not-a-url", CancellationToken.None);

        Assert.IsType<BadRequestObjectResult>(result);
    }
}

Key Takeaways