Captcha solver in C# with HttpClient: .NET example (2026)
In C#, a captcha solver is a small class around HttpClient. You post the sitekey and page URL to the Scarnox API, read the token from the JSON response, and submit it to the site's form. This guide builds that class for .NET 8, for PopularCaptcha, which Scarnox solves today on a proxyless basis.
Start with free trial credit, no card needed and try the code against a page you control before running it on anything else.
Requirements
- .NET 8 SDK (
dotnet --version). - A Scarnox API key, read from an environment variable.
- The sitekey of the page's PopularCaptcha widget. For help finding it, see how to find a captcha sitekey.
Create the project
dotnet new console -n CaptchaDemo
cd CaptchaDemo
No extra packages are needed. System.Net.Http and System.Text.Json are part of the framework.
The solver class
The class below owns one HttpClient with the Bearer header and a 185-second timeout. It returns the token or throws with the API's error text:
// ScarnoxSolver.cs
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
public sealed class ScarnoxSolver : IDisposable
{
private const string ApiUrl = "https://api.scarnox.com/api/tasks/create";
private readonly HttpClient _http;
public ScarnoxSolver(string apiKey)
{
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(185) };
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
}
public async Task<string> SolveAsync(
string sitekey,
string pageUrl,
bool invisible = false,
CancellationToken ct = default)
{
var body = new Dictionary<string, object>
{
["captcha_type"] = "PopularCaptchaTaskProxyless",
["site_key"] = sitekey,
["site_url"] = pageUrl,
};
if (invisible)
{
body["is_invisible"] = true;
}
// The call returns when the token is ready. There is no polling.
using var resp = await _http.PostAsJsonAsync(ApiUrl, body, ct);
var data = await resp.Content.ReadFromJsonAsync<CreateTaskResponse>(cancellationToken: ct)
?? throw new InvalidOperationException(
$"HTTP {(int)resp.StatusCode}: empty response");
if (data.ErrorId != 0)
{
throw new InvalidOperationException($"{data.ErrorCode}: {data.ErrorDescription}");
}
return data.Solution?.Token
?? throw new InvalidOperationException("Response had no token");
}
public void Dispose() => _http.Dispose();
}
public sealed class CreateTaskResponse
{
[JsonPropertyName("errorId")]
public int ErrorId { get; set; }
[JsonPropertyName("errorCode")]
public string? ErrorCode { get; set; }
[JsonPropertyName("errorDescription")]
public string? ErrorDescription { get; set; }
[JsonPropertyName("solution")]
public Solution? Solution { get; set; }
}
public sealed class Solution
{
[JsonPropertyName("token")]
public string? Token { get; set; }
}
The errorId check is the important line. The API returns 0 on success. Any other value comes with errorCode and errorDescription, and the class surfaces both.
Submit the token
The token goes to the site's form. Use a second HttpClient with a cookie container, so the session that loaded the form is reused on the POST:
// Program.cs
using System.Net;
var apiKey = Environment.GetEnvironmentVariable("SCARNOX_API_KEY")
?? throw new InvalidOperationException("Set SCARNOX_API_KEY");
const string pageUrl = "https://example.com/signup";
const string sitekey = "YOUR-SITEKEY-UUID";
using var solver = new ScarnoxSolver(apiKey);
string token = "";
// Retry transient failures. Failed solves are not charged.
for (int attempt = 1; attempt <= 3; attempt++)
{
try
{
token = await solver.SolveAsync(sitekey, pageUrl);
break;
}
catch (InvalidOperationException ex) when (attempt < 3)
{
Console.Error.WriteLine($"Attempt {attempt} failed: {ex.Message}");
await Task.Delay(TimeSpan.FromSeconds(attempt));
}
}
var cookies = new CookieContainer();
var handler = new HttpClientHandler { CookieContainer = cookies, UseCookies = true };
using var site = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(60) };
// GET first so the site sets its session cookie.
await site.GetAsync(pageUrl);
using var form = new FormUrlEncodedContent(new Dictionary<string, string>
{
["email"] = "you@example.com",
["h-captcha-response"] = token,
["g-recaptcha-response"] = token,
});
using var response = await site.PostAsync(pageUrl, form);
Console.WriteLine($"Status: {(int)response.StatusCode}");
Run it:
set SCARNOX_API_KEY=scarnox_YOURAPIKEY
dotnet run
On PowerShell use $env:SCARNOX_API_KEY = "scarnox_YOURAPIKEY" instead.
Why it is structured this way
**One HttpClient per solver.** HttpClient is meant to be reused, not created for every call. The solver owns one and disposes it at the end.
A 185-second timeout. The API blocks until the token is ready. A shorter timeout cancels solves that were about to finish.
Typed deserialization. ReadFromJsonAsync maps the JSON onto CreateTaskResponse, so the error and token checks read like normal C#.
The form submit. The site receives both h-captcha-response and g-recaptcha-response, since sites read one or the other. The cookie container keeps the session between the GET and the POST.
Timing
The token expires in about two minutes. Solve right before you submit. If you solve early in a batch and submit much later, the token will be stale. Captcha token expiry covers the details.
Invisible widgets
Pass invisible: true to SolveAsync when the widget is invisible. The request then includes is_invisible: true. Some pages need a JavaScript callback that a server-side client cannot trigger. In that case, a browser automation tool is the better choice. The invisible captcha guide covers the patterns.
Scraping responsibly
If the program scrapes public pages, check robots.txt and the site's terms first. Use a delay between requests, and keep only the personal data you need. For the legal background, see is web scraping legal.
Pricing
Scarnox bills per successful solve, and failed solves are free. See current pricing for the rates. Start with free trial credit, no card needed.