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:

C# · NoneCap.cs
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:

C# · Program.cs
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.

C# · Selenium WebDriver
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).

C# · Playwright for .NET
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.

C# · HttpClient form submit
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.

C# · bounded fan-out
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:

C# · enterprise rqdata
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:

C# · 2captcha-csharp compat
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?
Not yet. The official clients are npm (TypeScript) and PyPI (Python). From C# you call the REST API directly: two endpoints, Bearer auth, JSON in and out. The one-file 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#?
Read the sitekey with 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?
.NET 8 or later. The client uses 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?
At least one credit per solve. Billing starts at one credit per hCaptcha challenge round (hCaptcha decides how many: often one, sometimes two or three), charged only on success, so failed, expired, and cancelled solves are never charged. Credits run $0.20 to $0.50 per 1,000 depending on pack size, credits never expire, and no subscription is required. See pricing.
How many solves can I run in parallel?
Each account has a concurrency cap: 5 solves in flight on the free trial, rising with purchases and plans (the API reference lists every tier). Bound your fan-out with 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.

Start solving hCaptcha in minutes.

1,300 free credits on signup. Pay per solve, credits never expire, failed solves never charged.