Solve hCaptcha in C# and .NET.
The NoneCap hCaptcha solver is a REST API, so .NET needs no SDK:
one HttpClient sends the sitekey and page URL and gets back a real
P1_ token. Below is that client in one file, then the token going into
a page with Selenium WebDriver, with Playwright for .NET, and with no browser at all.
If your code already talks to 2Captcha, there is a short way across too.
What a C# program needs
Two endpoints: POST /v1/solves to create a solve and
GET /v1/solves/{id} to read it, with your API key as a Bearer
token and JSON both ways. The work happens on NoneCap’s side: you send the
sitekey and the page URL, and the response carries the token as a string. Your
process only places it in the form.
The examples target .NET 8 or later and use nothing outside the base library except
the browser package each section names. Put your key in NONECAP_KEY;
the code reads it from the environment, so it never sits in source control.
The client in one file
Records for the request and the solve, an exception that carries the API’s error code, and one method that returns when the solve is finished:
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
public sealed record SolveRequest(
string Type, // "hcaptcha" | "hcaptcha_enterprise"
string Sitekey,
string Url,
string? Rqdata = null, // enterprise, when the page supplies one
string? UserAgent = null,
string? Proxy = null);
public sealed record SolveError(string Code, string Message, bool Retryable);
public sealed record Solve(
string Id,
string Status, // pending | solving | solved | failed | expired | cancelled
string? Token,
string? UserAgent,
int? CreditsCharged,
SolveError? Error);
public sealed record ApiError(string Code, string Message, string? Param);
public sealed class NoneCapException(int status, ApiError? error)
: Exception($"NoneCap HTTP {status}: {error?.Code} {error?.Message}")
{
public int Status { get; } = status;
public ApiError? Error { get; } = error;
}
public sealed class NoneCapClient
{
static readonly JsonSerializerOptions Json = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
sealed record ErrorEnvelope(ApiError Error);
readonly HttpClient _http;
public NoneCapClient(string apiKey)
{
_http = new HttpClient
{
BaseAddress = new Uri("https://api.nonecap.com/v1/"),
Timeout = TimeSpan.FromSeconds(120),
};
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
}
public async Task<Solve> SolveAsync(SolveRequest request, CancellationToken ct = default)
{
// ?wait=90 holds the request open until the solve is terminal,
// so the token usually arrives on this first response.
var solve = await SendAsync(HttpMethod.Post, "solves?wait=90",
JsonContent.Create(request, options: Json), ct);
// A 202 means it was still running after 90 s: re-attach.
while (solve.Status is "pending" or "solving")
solve = await SendAsync(HttpMethod.Get, $"solves/{solve.Id}?wait=90", null, ct);
return solve;
}
public async Task<string> SolveHCaptchaAsync(
string sitekey, string url, string? userAgent = null, CancellationToken ct = default)
{
var solve = await SolveAsync(new SolveRequest("hcaptcha", sitekey, url, UserAgent: userAgent), ct);
if (solve.Status != "solved")
throw new InvalidOperationException(
$"solve {solve.Id} {solve.Status}: {solve.Error?.Code} {solve.Error?.Message}");
return solve.Token!;
}
async Task<Solve> SendAsync(HttpMethod method, string path, HttpContent? body, CancellationToken ct)
{
using var req = new HttpRequestMessage(method, path) { Content = body };
using var res = await _http.SendAsync(req, ct);
if (!res.IsSuccessStatusCode)
{
ApiError? error = null;
try { error = (await res.Content.ReadFromJsonAsync<ErrorEnvelope>(Json, ct))?.Error; }
catch (JsonException) { }
throw new NoneCapException((int)res.StatusCode, error);
}
return (await res.Content.ReadFromJsonAsync<Solve>(Json, ct))!;
}
} Calling it:
var nonecap = new NoneCapClient(Environment.GetEnvironmentVariable("NONECAP_KEY")!);
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
var token = await nonecap.SolveHCaptchaAsync(
"f5ab1c2d-7e8f-4a9b-b1c2-d3e4f5a6b7c8", "https://target.example/login", ct: cts.Token);
Console.WriteLine(token); // P1_eyJ0eXAi... ?wait=90 asks the server to hold the request until the solve reaches a
terminal state, so the token usually comes back on the first response and there is
no polling loop to write. If the solve is still running after 90 seconds the API
answers 202 with status pending or solving, and the loop
re-attaches with GET ?wait=90. The HttpClient timeout is set
to 120 seconds because the default of 100 is too close to a 90-second hold, and the
CancellationToken caps the whole wait.
JsonNamingPolicy.SnakeCaseLower maps the API’s snake_case fields
(user_agent, credits_charged) onto the records, and
WhenWritingNull leaves unset optional fields out of the request. A non-2xx
response becomes a NoneCapException with the API’s error
code, such as unauthorized, insufficient_credits
or concurrency_limit_exceeded; the error
reference lists them all. A solve that ends failed or
expired is not charged, so retrying costs time, not credits.
If you would rather not hold connections open (some proxies and serverless hosts cut
long requests), drop ?wait: the POST returns at once with status
pending, and you read GET /v1/solves/{id} every
couple of seconds until the status changes.
Selenium WebDriver for .NET
With Selenium.WebDriver (4.6 or later fetches the matching chromedriver for you), the flow is: read the sitekey off the page, get a token, write it into the form, submit.
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;
var nonecap = new NoneCapClient(Environment.GetEnvironmentVariable("NONECAP_KEY")!);
using var driver = new ChromeDriver();
driver.Navigate().GoToUrl("https://target.example/login");
var js = (IJavaScriptExecutor)driver;
// 1. Read the sitekey off the rendered widget.
var sitekey = driver.FindElement(By.CssSelector("[data-sitekey]")).GetDomAttribute("data-sitekey")!;
// 2. Get a token for this page, solved under this browser's user agent.
var userAgent = js.ExecuteScript("return navigator.userAgent;") as string;
var token = await nonecap.SolveHCaptchaAsync(sitekey, driver.Url, userAgent);
// 3. Inject it. The textarea lives in the top document, not in the
// widget iframe, so there is no frame to switch into.
js.ExecuteScript("""
const token = arguments[0];
for (const name of ['h-captcha-response', 'g-recaptcha-response']) {
const el = document.querySelector(`textarea[name='${name}']`);
if (el) el.value = token;
}
const cb = document.querySelector('[data-callback]')?.getAttribute('data-callback');
if (cb && typeof window[cb] === 'function') window[cb](token);
""", token);
// 4. Submit the form the way the page does.
var before = driver.Url;
driver.FindElement(By.CssSelector("button[type=submit]")).Click();
new WebDriverWait(driver, TimeSpan.FromSeconds(15)).Until(d => d.Url != before); GetDomAttribute reads the sitekey exactly as the page wrote it. The
h-captcha-response textarea is in the top document, not inside the
widget’s iframe, so one ExecuteScript call reaches it with no
SwitchTo().Frame(). The script also fills g-recaptcha-response,
which some backends read instead, and calls the page’s data-callback
function: invisible widgets submit from that callback, not from a field change.
The solve request carries the browser’s own navigator.userAgent.
Some sites only accept a token that was solved under the same browser version that
submits it; on the rest the field changes nothing. If a page keeps its sitekey out of
the DOM and passes it straight to hcaptcha.render(), read the
sitekey parameter off the hcaptcha.com/1/api.js script URL
instead; the sitekey guide shows where to look.
Playwright for .NET
The same steps with Microsoft.Playwright.
After adding the package and building once, install its browsers with
pwsh bin/Debug/net8.0/playwright.ps1 install (with your target framework in the path).
using Microsoft.Playwright;
var nonecap = new NoneCapClient(Environment.GetEnvironmentVariable("NONECAP_KEY")!);
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
await page.GotoAsync("https://target.example/login");
var sitekey = await page.Locator("[data-sitekey]").First.GetAttributeAsync("data-sitekey");
var userAgent = await page.EvaluateAsync<string>("() => navigator.userAgent");
var token = await nonecap.SolveHCaptchaAsync(sitekey!, page.Url, userAgent);
await page.EvaluateAsync("""
token => {
for (const name of ['h-captcha-response', 'g-recaptcha-response']) {
const el = document.querySelector(`textarea[name='${name}']`);
if (el) el.value = token;
}
const cb = document.querySelector('[data-callback]')?.getAttribute('data-callback');
if (cb && typeof window[cb] === 'function') window[cb](token);
}
""", token);
var before = page.Url;
await page.ClickAsync("button[type=submit]");
await page.WaitForURLAsync(url => url != before); EvaluateAsync passes the token in as the function’s argument, so
nothing is pasted into the script text. Waiting on WaitForURLAsync after
the click keeps the program alive until the form has gone through. The
Playwright guide covers telling hCaptcha apart
from reCAPTCHA and Turnstile in the DOM, which applies unchanged in .NET.
Without a browser: HttpClient and a form POST
Many sites don’t need a browser at all. Fetch the page, solve, and post the form
with the token in h-captcha-response. What matters is consistency: one
CookieContainer and one exit IP for the page load, the solve and the
submit, so give NoneCap the same proxy your HttpClient uses, and send the
solve’s returned user_agent as your User-Agent header.
using System.Net;
var nonecap = new NoneCapClient(Environment.GetEnvironmentVariable("NONECAP_KEY")!);
// One cookie jar and one exit for page load, solve and submit.
using var site = new HttpClient(new HttpClientHandler
{
CookieContainer = new CookieContainer(),
Proxy = new WebProxy("http://203.0.113.10:8080")
{
Credentials = new NetworkCredential("user", "pass"),
},
});
var html = await site.GetStringAsync("https://target.example/login");
var sitekey = System.Text.RegularExpressions.Regex.Match(html, "data-sitekey=\"([^\"]+)\"").Groups[1].Value;
var solve = await nonecap.SolveAsync(new SolveRequest(
"hcaptcha", sitekey, "https://target.example/login", Proxy: "http://user:pass@203.0.113.10:8080"));
if (solve.Status != "solved") throw new Exception($"{solve.Status}: {solve.Error?.Message}");
using var post = new HttpRequestMessage(HttpMethod.Post, "https://target.example/login")
{
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["email"] = "me@example.com",
["password"] = "…",
["h-captcha-response"] = solve.Token!,
["g-recaptcha-response"] = solve.Token!,
}),
};
post.Headers.TryAddWithoutValidation("User-Agent", solve.UserAgent);
using var res = await site.SendAsync(post);
Tokens are single-use and valid for about two minutes, so solve right before you
submit. The proxy guide covers the
proxy formats and why a token solved on one exit can fail when submitted
from another, and the web scraping guide walks
the whole fetch, solve and submit pipeline.
Many solves at once
Parallel.ForEachAsync runs solves side by side with a bound on how many
are in flight. Size MaxDegreeOfParallelism to your account’s
concurrency cap, not to your core count: the threads spend their time waiting on
I/O, and requests past the cap come back as 429s.
var nonecap = new NoneCapClient(Environment.GetEnvironmentVariable("NONECAP_KEY")!);
(string Sitekey, string Url)[] targets =
[
("f5ab1c2d-7e8f-4a9b-b1c2-d3e4f5a6b7c8", "https://target.example/login"),
("f5ab1c2d-7e8f-4a9b-b1c2-d3e4f5a6b7c8", "https://target.example/signup"),
];
// Keep MaxDegreeOfParallelism at or under your account's concurrency
// cap; past it the API answers 429 instead of solving faster.
var tokens = new string[targets.Length];
await Parallel.ForEachAsync(Enumerable.Range(0, targets.Length),
new ParallelOptions { MaxDegreeOfParallelism = 5 },
async (i, ct) => tokens[i] = await nonecap.SolveHCaptchaAsync(targets[i].Sitekey, targets[i].Url, ct: ct)); Enterprise sitekeys
Some enterprise deployments pass a fresh rqdata value into
hcaptcha.render() or execute(). When the page supplies one,
send it with the enterprise type; the request record already has the field:
var solve = await nonecap.SolveAsync(new SolveRequest(
"hcaptcha_enterprise", sitekey, pageUrl, Rqdata: rqdata));
Capture rqdata right before solving; it is tied to one challenge. The
rqdata guide shows where it lives
in the page.
Already using a 2Captcha C# client?
NoneCap answers 2Captcha’s in.php / res.php API at
https://api.nonecap.com/compat/2captcha/, with your NoneCap API key as
key and method=hcaptcha. Hand-written code that builds
those URLs only needs the new host and key. The
2captcha-csharp
package has the 2captcha.com host built in, but it lets you replace its
ApiClient, so a small subclass sends the same calls to NoneCap:
using TwoCaptcha.Captcha;
using TwoCaptcha.Exceptions;
var solver = new TwoCaptcha.TwoCaptcha(Environment.GetEnvironmentVariable("NONECAP_KEY"));
solver.SetApiClient(new NoneCapCompatClient());
solver.PollingInterval = 5;
var captcha = new HCaptcha();
captcha.SetSiteKey("f5ab1c2d-7e8f-4a9b-b1c2-d3e4f5a6b7c8");
captcha.SetUrl("https://target.example/login");
await solver.Solve(captcha);
Console.WriteLine(captcha.Code); // the hCaptcha token
// 2captcha-csharp has no host setting, but its ApiClient is replaceable:
// send its in.php / res.php calls to NoneCap's compat host instead.
class NoneCapCompatClient : TwoCaptcha.ApiClient
{
static readonly HttpClient Http = new()
{
BaseAddress = new Uri("https://api.nonecap.com/compat/2captcha/"),
};
public override async Task<string> In(Dictionary<string, string> parameters, Dictionary<string, FileInfo> files)
{
using var res = await Http.PostAsync("in.php", new FormUrlEncodedContent(parameters));
return Check(await res.Content.ReadAsStringAsync());
}
public override async Task<string> Res(Dictionary<string, string> parameters)
{
var query = string.Join("&", parameters.Select(p => $"{p.Key}={Uri.EscapeDataString(p.Value)}"));
return Check(await Http.GetStringAsync("res.php?" + query));
}
static string Check(string body) => body.StartsWith("ERROR_") ? throw new ApiException(body) : body;
}
The endpoint accepts sitekey, pageurl, data
(rqdata), userAgent, proxy and proxytype, and
res.php answers get, get2,
reportgood, reportbad and getbalance (in
credits). Other captcha methods answer ERROR_METHOD_DOES_NOT_EXIST.
Solves made this way bill and show in your dashboard like any other. The
API reference has the full
mapping, and NoneCap vs 2Captcha compares the two
services.
Scope, honestly
NoneCap solves hCaptcha only: regular, invisible, and enterprise sitekeys. If your C# program is facing reCAPTCHA or Cloudflare Turnstile, this API won’t help today; both are on the roadmap, not live. Check which widget the page loads before writing the integration.
Last updated November 2026.
Frequently asked
Is there an official C# or .NET SDK?
HttpClient client on this page is the whole integration, and the request and response shapes are in the API reference. If you already use a 2Captcha C# client, it can talk to the 2Captcha-compatible endpoint instead.How do I solve hCaptcha in Selenium with C#?
driver.FindElement(By.CssSelector("[data-sitekey]")).GetDomAttribute("data-sitekey"), send it with driver.Url to POST /v1/solves, then write the returned token into the h-captcha-response textarea with IJavaScriptExecutor.ExecuteScript and fire the data-callback function if the page sets one. The textarea is in the top document, so there is no SwitchTo().Frame(). The Selenium example on this page does all four steps.Which .NET version do the examples need?
JsonNamingPolicy.SnakeCaseLower (added in .NET 8) to map the API’s snake_case fields onto C# records, and the samples use C# 12 primary constructors and collection expressions. On older runtimes the same two HTTP calls work; map the JSON fields by hand with [JsonPropertyName] attributes.How much does each solve cost?
How many solves can I run in parallel?
Parallel.ForEachAsync and MaxDegreeOfParallelism, or a SemaphoreSlim, sized to that cap. Requests over it get a 429 concurrency_limit_exceeded: retry once a solve finishes.