Use the CAPTCHA API with Selenium in C#

By solvethree4 min read

Call solvethree with HttpClient, wait for a ready task, then put solution.token into your application's CAPTCHA field. Chrome must use the same proxy and the returned browser identity before opening the target page.

The example below uses .NET 8 and Selenium Chrome to submit an owned form. It's a direct REST integration, not an official C# SDK or Selenium plugin.

On this page

Prepare the project and target

  • Use a .NET 8 console project with its default implicit usings enabled. Add the NuGet packages Selenium.WebDriver and Selenium.Support, both at version 4.52.0.
  • Install Chrome locally. Selenium Manager supplies a compatible Chrome driver.
  • Make sure the target hostname has an active site profile available to your workspace. Sending a hostname and action doesn't create one; use the site profile setup instructions first.
  • Use an IP-allowlisted proxy without credentials in its URL. The example passes that same proxy to solvethree and Chrome.
Environment variables used by the example
Environment variables used by the example
VariableWhat to supply
SOLVETHREE_API_KEYYour workspace API key. Keep it on the trusted machine running the automation, not in the page.
SOLVETHREE_PROXYThe IP-allowlisted proxy URL used for both solving and browser submission.
TARGET_URLThe full URL of your application's form page. The example extracts its hostname for the task's site.

Change the example's action from login if your integration uses another action. It relies on the default recaptchaType, enterprise-v3; the task request doesn't need a type or url field.

Create the task and submit the form

The API helper authenticates with your workspace key and rejects non-success HTTP responses before parsing a success payload. The program creates one task, polls its ID while the status is pending or solving, and starts Chrome only after the task is ready.

C# · create a task and submit the owned form
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json.Nodes;

var key = Environment.GetEnvironmentVariable("SOLVETHREE_API_KEY")
    ?? throw new Exception("Set SOLVETHREE_API_KEY");
var proxy = Environment.GetEnvironmentVariable("SOLVETHREE_PROXY")
    ?? throw new Exception("Set SOLVETHREE_PROXY");
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(180));

async Task<JsonObject> Api(HttpMethod method, string path, JsonObject? body = null)
{
    using var request = new HttpRequestMessage(method, "https://api.solvethree.com/v1" + path);
    if (body is not null)
        request.Content = new StringContent(body.ToJsonString(), Encoding.UTF8, "application/json");
    using var response = await http.SendAsync(request, deadline.Token);
    response.EnsureSuccessStatusCode();
    return JsonNode.Parse(await response.Content.ReadAsStringAsync(deadline.Token))!.AsObject();
}

var requestId = Guid.NewGuid().ToString(); // Save with these inputs for recovery.
var input = new JsonObject {
    ["site"] = new Uri(Environment.GetEnvironmentVariable("TARGET_URL") ?? throw new Exception("Set TARGET_URL")).Host, ["action"] = "login",
    ["proxy"] = proxy, ["requestId"] = requestId
};
var task = await Api(HttpMethod.Post, "/tasks?waitMs=0", input);
while (task["status"]!.GetValue<string>() is "pending" or "solving")
{
    var id = Uri.EscapeDataString(task["id"]!.GetValue<string>());
    task = await Api(HttpMethod.Get, "/tasks/" + id + "?waitMs=15000");
    if (task["status"]!.GetValue<string>() is "pending" or "solving")
        await Task.Delay(1000, deadline.Token);
}
if (task["status"]!.GetValue<string>() != "ready")
    throw new Exception("Task failed: " + task["errorCode"]?.GetValue<string>()
        + ": " + task["error"]?.GetValue<string>());
var solution = task["solution"]!.AsObject();
// Use solution["token"], ["userAgent"] and ["secChUa"] with the original proxy.
Console.WriteLine("Ready task: " + task["id"]!.GetValue<string>());
var target = Environment.GetEnvironmentVariable("TARGET_URL") ?? throw new Exception("Set TARGET_URL");
if (!string.IsNullOrEmpty(new Uri(proxy).UserInfo)) throw new Exception("Use an IP-allowlisted proxy");
var options = new ChromeOptions();
options.AddArgument("--proxy-server=" + proxy);
options.AddArgument("--user-agent=" + solution["userAgent"]!.GetValue<string>());
using var browser = new ChromeDriver(options);
browser.ExecuteCdpCommand("Network.enable", new Dictionary<string, object>());
browser.ExecuteCdpCommand("Network.setExtraHTTPHeaders", new Dictionary<string, object> {
    ["headers"] = new Dictionary<string, object> { ["sec-ch-ua"] = solution["secChUa"]!.GetValue<string>() }
});
browser.Navigate().GoToUrl(target);
var wait = new WebDriverWait(browser, TimeSpan.FromSeconds(20));
var field = wait.Until(driver => driver.FindElement(By.Name("g-recaptcha-response")));
browser.ExecuteScript("arguments[0].value=arguments[1]; arguments[0].dispatchEvent(new Event('input',{bubbles:true}));",
    field, solution["token"]!.GetValue<string>());
browser.FindElement(By.CssSelector("button[type=submit]")).Click();
wait.Until(driver => driver.FindElement(By.Id("success"))); // Your application's success marker.

The 30-second HTTP timeout, 180-second API deadline, and 20-second Selenium wait are settings in this example, not service timing guarantees. Adjust them for your application's timeout policy.

Match the handoff to your application

  1. 1.

    Keep the proxy and returned identity together

    ChromeOptions applies the original proxy and solution.userAgent. The CDP commands enable networking and set sec-ch-ua from solution.secChUa before navigation. Keep that order; don't open the page first and change the identity afterward.

  2. 2.

    Write the token where the application reads it

    The example locates a field named g-recaptcha-response, assigns solution.token, and dispatches an input event. Replace this handoff if your application reads the token from somewhere else. Filling a field only helps if the outgoing application request actually includes its value.

  3. 3.

    Replace the submit and success selectors

    The sample clicks button[type=submit] and waits for an element with ID success. Use selectors from your real application, and choose a success marker that confirms the submitted operation succeeded—not merely that the button was clicked.

A ready task means a solution is available. The post-submit check is separate: the target still decides whether to accept the application request.

Handle failures without creating unnecessary tasks

The API helper throws on an HTTP response.

Inspect the HTTP status and error response. This is different from a task reaching failed.

Try this: Correct the reported request or authentication problem before retrying. Don't keep resending unchanged invalid inputs.

The task reaches failed.

Read errorCode and error; site-unreachable is one possible task error.

Try this: Stop polling that task and address the reported cause. Failed solves are refunded.

The task is ready, but the target rejects the submission.

Inspect the outgoing application request first: does it contain solution.token, rather than the task ID or an old token? Then check the proxy, user agent, client hints, and target URL.

Try this: Correct the token handoff or identity mismatch before paying for another solve.

Selenium times out after clicking submit.

Inspect the page and application response. The success selector is a fixture, not a universal acceptance signal.

Try this: Replace it with your application's actual success condition, or handle the rejection the application returned.

The sample generates a requestId, but doesn't persist it for you. Save it with the exact task inputs before creation if you need timeout recovery. If creation times out, resend the same inputs with that same ID to recover the existing task; don't generate a new ID just because the response was lost. Reusing an ID with different inputs conflicts. If you already have the task ID, retrieve that task instead.

Run your first task.

Start with $5 in workspace credit.

Create a workspace