CAPTCHA Solving 12 min read

Solving reCAPTCHA in C# with the 2Captcha Service

Step-by-step 2Captcha integration in C#: send reCAPTCHA tasks to the API, poll for solution tokens, and submit forms automatically. Get the full code example.

ST
Scraping.Pro Team
Data collection for business needs
Published: 29 June 2026

This guide shows how to pass a Google reCAPTCHA from a C# application using the 2captcha solving service. It walks through the workflow, how to tell which kind of reCAPTCHA is on the page, two ways to call the API (the official NuGet library and a plain HttpClient), how to feed the token back into a real page, and the errors you will actually hit once this runs in production.

If you prefer Python, the same idea is explained step by step in the companion article: 2captcha service to solve reCAPTCHA.


Why use a captcha-solving service?

reCAPTCHA exists to block bots. When your automation is legitimate (QA runs against your own registration and checkout flows, uptime monitoring, or collecting data you are permitted to collect) that same protection stands in the way, and there is no human at the keyboard to click the checkbox.

A solving service fills that gap. You hand 2captcha the captcha's parameters, it returns a valid answer token, and you place that token into the page the same way a real browser would.

How does the answer actually get produced? Despite the industry's recent "AI solver" marketing, 2captcha still leans heavily on a distributed network of human workers: its own description puts the worker pool in the millions, and the company's own comparison of AI versus human solving argues that fully autonomous AI "loses in both accuracy and adaptability," routing the harder challenges to people. So treat it as a hybrid. Some categories are handled by models, anything awkward goes to a human, and the response you get back is the same token either way.

Use it responsibly. Solving captchas to bypass protections on sites you do not own, or in violation of a site's terms of service, can be illegal. Stick to systems you own, testing environments, or data you are explicitly permitted to access.


Which reCAPTCHA are you dealing with?

Identify the variant before you write any solving code, because it decides which parameters you send.

  • reCAPTCHA v2 (the "I'm not a robot" checkbox): look for a <div class="g-recaptcha"> carrying a data-sitekey attribute, or a request to www.google.com/recaptcha/api2/anchor with a k= parameter. That value is your site key.
  • reCAPTCHA v2 Invisible: the same widget with no visible checkbox; it fires on a button click or on page load, and a badge sits in the corner.
  • reCAPTCHA v3: no challenge at all. The page calls grecaptcha.execute(sitekey, { action: '...' }) and scores you from 0.0 to 1.0. You will see the v3 badge and an action name in the JavaScript.
  • reCAPTCHA Enterprise: the script calls grecaptcha.enterprise.execute(...). You set an enterprise flag when you solve it.

The site key is public by design, so reading it from the DOM or the network tab is expected rather than a trick. The page URL you send is simply the full address of the page where the captcha appears.


How the reCAPTCHA v2 workflow works

Solving a v2 reCAPTCHA is a four-step loop, and it looks the same in any language:

  1. Read the parameters. You need the site key (from data-sitekey, as above) and the full page URL where the captcha appears.

  2. Submit the task. Send those two values to 2captcha. It queues the job and returns a captcha ID.

  3. Poll for the answer. Solving is not instant. A v2 usually comes back in roughly 10 to 60 seconds, occasionally longer when the queue is busy. Poll the service with the captcha ID until it returns the token (the g-recaptcha-response value).

  4. Use the token. Put the token into the page and let the site verify it with Google, exactly as it would for a real visitor.

One detail trips people up constantly: the token is single-use and expires about two minutes after it is issued. Solve early and submit late, and Google rejects a token that looked perfectly valid. Solve as close to the moment of submission as you can.

The same shape covers v2 Invisible, v2 with a callback, and Enterprise. Add an action and a score and it covers v3 as well.


Two ways to integrate

You can talk to 2captcha in two ways:

Approach When to use it
Official C# library (NuGet 2captcha-csharp) Recommended for almost everything. It handles submission, polling, timeouts, proxies, error types, and every captcha type behind a clean async API.
Raw HTTP API Useful when you cannot add a dependency, want full control, or are learning how the service behaves underneath.

2captcha exposes two HTTP APIs. The original one lives at https://2captcha.com/in.php and https://2captcha.com/res.php (now labelled API v1). The newer JSON API lives at https://api.2captcha.com/createTask and https://api.2captcha.com/getTaskResult (API v2). Both are live, and the official library wraps whichever it uses so you never touch the difference. For new raw integrations, the v2 JSON API is what 2captcha documents as current; v1 is easier to read and still fully functional, which is why the raw example below uses it. Whichever you pick, use HTTPS so your API key never travels in clear text.


Install the package:

bash
dotnet add package 2captcha-csharp

Solving a reCAPTCHA v2 is then a few lines. The library submits the task, polls for the result, and hands you the token through captcha.Code:

c#
using System;
using System.Threading.Tasks;
using TwoCaptcha.Captcha;

class Program
{
    static async Task Main(string[] args)
    {
        if (args.Length != 1)
        {
            Console.WriteLine("Usage: app <2captcha_api_key>");
            return;
        }

        var solver = new TwoCaptcha(args[0]);

        var captcha = new ReCaptcha();
        captcha.SetSiteKey("6Lf5CQkTAAAAAKA-kgNm9mV6sgqpGmRmRMFJYMz8");
        captcha.SetUrl("https://example.com/page-with-recaptcha");

        try
        {
            await solver.Solve(captcha);
            Console.WriteLine("Token: " + captcha.Code);
            // captcha.Code now holds the g-recaptcha-response token.
            // Submit it to the target site while it is still fresh.
        }
        catch (Exception e)
        {
            Console.WriteLine("Solving failed: " + e.Message);
        }
    }
}

Useful configuration knobs on the TwoCaptcha instance:

c#
var solver = new TwoCaptcha("YOUR_API_KEY");
solver.DefaultTimeout    = 120;  // seconds to wait for non-reCAPTCHA results
solver.RecaptchaTimeout  = 600;  // seconds to wait for reCAPTCHA results
solver.PollingInterval   = 10;   // seconds between polls (keep >= 5)

The same ReCaptcha object handles the variations through extra setters:

c#
captcha.SetInvisible(true);    // reCAPTCHA v2 Invisible
captcha.SetEnterprise(true);   // reCAPTCHA Enterprise
captcha.SetVersion("v3");      // reCAPTCHA v3
captcha.SetAction("verify");   // v3 action name
captcha.SetScore(0.7);         // v3 minimum score
captcha.SetProxy("HTTPS", "login:password@IP_address:PORT");

Two more methods you will reach for often: check your balance with await solver.Balance(), and grade an answer with await solver.Report(captcha.Id, true) for a good token or await solver.Report(captcha.Id, false) for a bad one. Reporting a bad token refunds it and feeds back into future solving on your account.


Option B --- Raw HTTP API with HttpClient

If you would rather not pull in a dependency, here is a self-contained implementation using HttpClient, async/await, and System.Text.Json. It replaces the old HttpWebRequest/WebClient style and talks to the API v1 in.php/res.php endpoints in JSON mode (json=1).

c#
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

public sealed class TwoCaptchaClient
{
    private static readonly HttpClient Http = new HttpClient();
    private readonly string _apiKey;

    public TwoCaptchaClient(string apiKey) => _apiKey = apiKey;

    // Step 1: submit the reCAPTCHA task, receive a captcha id.
    public async Task<string> SubmitAsync(string siteKey, string pageUrl)
    {
        var url = "https://2captcha.com/in.php" +
                  $"?key={_apiKey}" +
                  "&method=userrecaptcha" +
                  $"&googlekey={siteKey}" +
                  $"&pageurl={Uri.EscapeDataString(pageUrl)}" +
                  "&json=1";

        var json = await Http.GetStringAsync(url);
        var root = JsonDocument.Parse(json).RootElement;

        if (root.GetProperty("status").GetInt32() != 1)
            throw new Exception("Submit error: " + root.GetProperty("request").GetString());

        return root.GetProperty("request").GetString(); // the captcha id
    }

    // Step 2 & 3: poll until the token is ready (or we give up).
    public async Task<string> GetTokenAsync(string captchaId, int maxAttempts = 24, int delayMs = 5000)
    {
        var url = "https://2captcha.com/res.php" +
                  $"?key={_apiKey}&action=get&id={captchaId}&json=1";

        for (var attempt = 1; attempt <= maxAttempts; attempt++)
        {
            await Task.Delay(delayMs);

            var json = await Http.GetStringAsync(url);
            var root = JsonDocument.Parse(json).RootElement;
            var request = root.GetProperty("request").GetString();

            if (root.GetProperty("status").GetInt32() == 1)
                return request; // solved: this is the g-recaptcha-response token

            // CAPCHA_NOT_READY is 2captcha's real spelling, not a typo here.
            // It simply means "still solving", so keep polling.
            if (request != "CAPCHA_NOT_READY")
                throw new Exception("Polling error: " + request);

            Console.WriteLine($"Still solving... ({attempt * delayMs / 1000}s elapsed)");
        }

        throw new TimeoutException("Captcha was not solved in time.");
    }
}

A note on CAPCHA_NOT_READY: the missing second "T" is intentional. That is the exact string the API returns while a captcha is still in the queue, so compare against it verbatim.

Getting the token onto the page

The last step is where naive tutorials go wrong. It is tempting to POST the token straight to the form:

c#
// Illustrative only. This works for a bare server-rendered form and
// almost never for a real login or checkout page (see below).
public async Task<string> SubmitFormAsync(string pageUrl, string token)
{
    var form = new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string, string>("g-recaptcha-response", token),
        new KeyValuePair<string, string>("submit", "submit"),
    });

    var response = await Http.PostAsync(pageUrl, form);
    return await response.Content.ReadAsStringAsync();
}

Real pages rarely accept that. A live form carries other fields you must include (username, password, hidden inputs, CSRF or anti-forgery tokens, __VIEWSTATE on ASP.NET). Many reCAPTCHA integrations also do not read a POST field at all: the token has to land in the hidden g-recaptcha-response element, and the site's own JavaScript callback has to fire before the page will submit or send its AJAX request. When your target is a browser-driven page (which is most of them), drive it with a real browser instead of forging the POST. That is the next section.

The same task on the v2 JSON API

For reference, the v2 createTask flow that 2captcha documents as current looks like this:

text
POST https://api.2captcha.com/createTask
{
  "clientKey": "YOUR_API_KEY",
  "task": {
    "type": "RecaptchaV2TaskProxyless",
    "websiteURL": "https://example.com/page-with-recaptcha",
    "websiteKey": "6Lf5CQkTAAAAAKA-kgNm9mV6sgqpGmRmRMFJYMz8"
  }
}

You get back {"errorId":0,"taskId":123456}, then poll https://api.2captcha.com/getTaskResult with that taskId until the response reads {"errorId":0,"status":"ready","solution":{"gRecaptchaResponse":"03AGdBq2..."}}. The advantage over v1 is that the key travels in the JSON body rather than the query string, so it stays out of proxy and server logs.


Putting the token into a real browser

Most C# automation that needs a captcha is already driving a browser with Selenium or Playwright. In that setup you do not submit a forged POST; you inject the solved token into the live page and let the site's own code take over. Read the site key from the DOM, solve it, drop the token into the hidden field, fire the widget's callback if it has one, then submit.

c#
using System.Threading.Tasks;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using TwoCaptcha.Captcha;

static async Task SolveInBrowser(string apiKey)
{
    using var driver = new ChromeDriver();
    driver.Navigate().GoToUrl("https://example.com/login");

    // 1. Read the site key straight from the widget.
    var widget  = driver.FindElement(By.CssSelector(".g-recaptcha, [data-sitekey]"));
    var siteKey = widget.GetAttribute("data-sitekey");

    // 2. Solve it with 2captcha.
    var solver  = new TwoCaptcha(apiKey);
    var captcha = new ReCaptcha();
    captcha.SetSiteKey(siteKey);
    captcha.SetUrl(driver.Url);
    await solver.Solve(captcha);
    var token = captcha.Code;

    // 3. Drop the token into the hidden field reCAPTCHA created.
    ((IJavaScriptExecutor)driver).ExecuteScript(
        "document.getElementById('g-recaptcha-response').value = arguments[0];", token);

    // 4. If the widget declares a callback, invoke it. Invisible widgets
    //    almost always need this; a plain checkbox form often does not.
    var callback = widget.GetAttribute("data-callback");
    if (!string.IsNullOrEmpty(callback))
        ((IJavaScriptExecutor)driver).ExecuteScript($"{callback}(arguments[0]);", token);

    // 5. Submit the form the way a user would.
    driver.FindElement(By.CssSelector("button[type=submit]")).Click();
}

Whether you need step 4 depends on the target. Test it: submit with only the field set, and if nothing happens, read data-callback and call it. Because the token expires in about two minutes, keep the solve and the submit close together rather than solving a batch up front.


Handling errors

The happy path is short; production is where the error strings show up. 2captcha reports problems as ERROR_* codes in the request field (v1) or as an errorId/errorCode (v2). These are the ones worth handling explicitly:

Code What it means What to do
CAPCHA_NOT_READY Not an error. The captcha is still being solved. Keep polling, no faster than every 5 seconds.
ERROR_KEY_DOES_NOT_EXIST The API key is wrong or inactive. Check you are sending your real 2captcha key.
ERROR_ZERO_BALANCE No funds on the account. Top up before the run; watch balance with Balance().
ERROR_NO_SLOT_AVAILABLE Your bid is too low or your queue is too long. Back off and retry; lower concurrency, or raise the bid.
ERROR_RECAPTCHA_INVALID_SITEKEY The site key you sent is not valid. Re-read data-sitekey; make sure you copied it in full.
ERROR_CAPTCHA_UNSOLVABLE Three workers failed; the price is auto-refunded. Retry, double-check the site key and page URL, add a proxy if the token is IP-bound.

Treat CAPCHA_NOT_READY as "wait", not "fail", and give everything else a real branch. A generic catch that swallows ERROR_ZERO_BALANCE will look like a mysterious hang at 3 a.m.


Practical tips

Escape the page URL before it goes into a query string (Uri.EscapeDataString). A stray &, ?, or = in the URL will corrupt the request otherwise.

Send the page URL exactly. A mismatch between the URL you solve against and the one you submit from is one of the most common reasons a token is rejected.

Reuse a single HttpClient. Creating a new one per request can exhaust sockets, so a static instance like the one above is the pattern to follow.

Keep your API key out of source. Read it from an environment variable, dotnet user-secrets, or a secrets manager, and never commit it.

Use a proxy when the token is IP-bound. Some Google properties tie the token to the solving IP, so pass SetProxy and solve through the same address you will submit from.

Report bad tokens. A rejected token reported with Report(id, false) is refunded and improves solving for your account over time.


Running it reliably in production

A one-off script can afford to be naive; a monitoring job that runs every five minutes cannot. A few habits keep a 2captcha integration stable:

  • Retry with backoff. Wrap submit-and-poll so that a transient ERROR_NO_SLOT_AVAILABLE or network blip retries a couple of times before it fails the run.
  • Tune timeouts per type. v3 and Enterprise can take longer than a plain v2. The library's RecaptchaTimeout default of 600 seconds is generous; lower it if you would rather fail fast and retry.
  • Close the report loop. When your target rejects a token, call Report(id, false). You recover the cost and nudge future accuracy upward.
  • Measure. Log solve time, success rate, and cost per solve. Those three numbers tell you when the target changed its captcha, when your bid is too low, and whether the integration still pays for itself.
  • Respect concurrency limits. If you fan out, expect ERROR_NO_SLOT_AVAILABLE and throttle accordingly rather than hammering the queue.

When you do not need a solver at all

A solving service is a cost and a dependency, so it is worth ruling out the alternatives first. If the site offers an official API or a data feed, use it. If you own the target, you can request access or whitelist your test IPs instead of solving anything. For flows behind reCAPTCHA v3, a stable, well-behaved session sometimes scores highly enough on its own, and if it is your own site you can simply lower the score threshold for trusted traffic. Reach for 2captcha when none of those apply.


A note on other services

If you ever need a fallback, most competing services (Anti-Captcha, CapMonster, CapSolver, and others) deliberately mirror 2captcha's API, both the v1 in.php/res.php shape and the v2 createTask JSON. In practice the code above ports to them with little more than a base-URL change, which makes it cheap to add a second provider for redundancy.


Summary

The mechanics of solving a reCAPTCHA in C# are not the hard part. The official 2captcha-csharp package is the right default for production, and a small HttpClient client is worth writing when you want zero dependencies or a clear view of the API. What actually decides whether the integration holds up is the surrounding detail: identifying the right captcha variant, injecting the token into a real browser rather than forging a POST, submitting before the token expires, and handling the ERROR_* codes instead of catching everything. Get those right, keep your usage within what you are permitted to automate, and the captcha stops being the thing that breaks your automation.

For the Python equivalent and more background on the service, see 2captcha service to solve reCAPTCHA.