How to Solve ALTCHA in C# and Post the Token Back Unchanged

To solve ALTCHA in C# there is nothing to read. ALTCHA is proof of work, not recognition: the site hands out a challenge and the client has to brute-force the number that satisfies it. There is no image, no audio and no guess involved, which makes a solve deterministic and fast. Either it finds the answer, or the challenge was malformed or had already expired. CapSkip added ALTCHA in version 1.2.6, and the .NET SDK exposes it as one method that takes the page URL plus the challenge. The part that actually catches people is what happens after: the token has to go back into the form exactly as the solver returned it.
What you need
- CapSkip 1.2.6 or later running on a Windows machine. ALTCHA support arrived in that release.
- The CapSkip .NET package, which targets .NET Standard 2.0, so .NET Framework 4.6.1 and up, .NET Core 2.0 and up, and .NET 6 and later.
- The URL of the page the widget sits on, and the endpoint that widget fetches its challenge from.
- An address for the solver. Local mode answers on 127.0.0.1 for that device only; Server mode listens on your network address or public IP so another box can reach it. Step 4 covers which one you want, and both live under connection settings.
# dotnet add package CapSkip dotnet add package CapSkip
Step 1: find the endpoint the ALTCHA widget calls
Everything else depends on this one value, so get it first. Open DevTools, go to the Network tab and reload the page the widget sits on. The widget makes a request for its challenge, usually to a path with altcha in it. That request URL is what you pass to the solver, and the JSON that endpoint returns is the challenge document itself, which you can pass instead of the URL.
Do not guess the attribute that names it, because it changed between widget generations. Read the page source.
| Widget generation | Attribute that names the challenge |
|---|---|
| v1 and v2 | challengeurl for an endpoint, with a separate challengejson attribute for an inline challenge |
| v3 and later | challenge, and that same attribute takes either a URL or the challenge data |
The three display styles, native, checkbox and switch, are purely visual. They all submit the same payload and the difference never reaches the solver, so you do not have to work out which one you are looking at. ALTCHA covers the widget attributes in its own integration docs.
Step 2: the solve call, and the two ways to supply a challenge
One method, two arguments: the page URL, then an options dictionary carrying the challenge. Give it the endpoint and CapSkip fetches the challenge for you.
// dotnet add package CapSkip
using CapSkip;
var solver = new CapSkipClient(host: "127.0.0.1", port: 8080);
// CapSkip fetches the challenge, then brute-forces the counter.
var result = await solver.AltchaAsync(
"https://example.com/signup",
new Dictionary<string, object?>
{
["challenge_url"] = "https://example.com/altcha/challenge",
});
Console.WriteLine(result.Token); // base64 payload for the form field
Console.WriteLine(result.Number); // the counter that satisfied itTwo fields on the result are ALTCHA only. Token is the base64 payload the form wants, and Number is the counter that solved the challenge. The Code property carries the same string as Token, so either works, but Token is named for the field it goes into and reads better at the call site. The GeeTest fields and the Turnstile user agent stay null here.
Number is worth logging. It is reported for both ALTCHA generations even though their payloads differ: a legacy token carries the counter at the top level, while a proof-of-work v2 token does not, keeping it inside a solution object instead. CapSkip reads it out of the solution object in its own API response, which reports both generations the same way.
Passing the challenge document instead
If your code already fetched the challenge, pass the document and no network request happens at all. This is the faster path when you are already scraping the page, and the one to use when the challenge arrives embedded in the HTML rather than from an endpoint.
// No fetch happens: the document is already here.
var result = await solver.AltchaAsync(
"https://example.com/signup",
new Dictionary<string, object?>
{
["challenge_json"] = new Dictionary<string, object>
{
["algorithm"] = "SHA-256",
["challenge"] = "YOUR_CHALLENGE_HASH",
["salt"] = "YOUR_SALT",
["signature"] = "YOUR_SIGNATURE",
["maxnumber"] = 1000000,
},
});That option takes a dictionary, which is serialised for you, or a JSON string if you have one already. Sending both the endpoint and the document is allowed, and the inline document wins, because fetching would only re-obtain what you just supplied. The two paths also behave differently under load: an inline challenge that has already expired is refused straight away rather than hashed pointlessly, while an endpoint lets the solver fetch a fresh challenge if the first one died while the job sat in the queue.
Which algorithms the solver covers
The same method handles both generations. The legacy scheme is covered with SHA-1, SHA-256, SHA-384 and SHA-512, and proof-of-work v2 is covered with PBKDF2 and iterative SHA. PBKDF2 is the default that ALTCHA itself recommends, so that covers the large majority of live sites.
Argon2id and scrypt are the exceptions, and they are refused rather than attempted: a task using one comes back in about a third of a second with ERROR_CAPTCHA_UNSOLVABLE and is never retried. That is deliberate. A memory-hard function is not something a retry fixes, so failing immediately beats looking busy. For ALTCHA that result points at the algorithm rather than an unreadable image, and the error code has a guide of its own.
Step 3: post the token back unchanged, before it expires
The widget submits its payload in a form field named altcha, so that is where your token goes. This is the step that quietly breaks.
// Send it exactly as it came back: no trimming,
// no re-encoding, no reordering.
var body = new FormUrlEncodedContent(new Dictionary<string, string>
{
["email"] = "[email protected]",
["altcha"] = result.Token!,
});
var response = await http.PostAsync("https://example.com/signup", body);The token is base64 of a JSON document whose fields are covered by the server’s own HMAC signature. Any modification invalidates it, so anything that looks like tidying up will break the submit: trimming whitespace, decoding and re-encoding it, or rebuilding the JSON with the keys in a different order. Some integrations read the payload out of a JSON body field rather than a form field, so check what the page’s own submit sends and mirror that.
The other way this step fails is timing. Challenge windows are short and some sites close them inside two minutes, and when one expires the site refuses the answer with a bare verification failure that looks exactly like a wrong answer. There is nothing in the error to tell you which of the two happened. Three habits avoid it: fetch the challenge immediately before solving rather than at the top of a long run, submit the token in the same unit of work that solved it, and never hold a token while a person fills in a form.
The client’s own polling timeouts are not what limits you here, because both are far longer than that two minute window. ALTCHA is CPU work rather than a browser session, so it runs on the default polling timeout and not the longer reCAPTCHA one.
| Constructor option | Default | What it covers |
|---|---|---|
| defaultTimeout | 120 seconds | ALTCHA and image CAPTCHA polling |
| recaptchaTimeout | 300 seconds | reCAPTCHA, Turnstile and GeeTest polling |
| pollingInterval | 5 seconds maximum | Polling starts at 0.25 seconds and backs off to this |
Step 4: where the solver runs, and which connection mode that needs
The samples above use 127.0.0.1 because that is right when your code and the solver share a machine. As soon as the code that calls the solver runs somewhere else, such as a container, a build agent, a VPS or a managed host, loopback no longer points at the solver, and the first solve throws a NetworkException.
Switch CapSkip to Server mode and it listens on your network address or public IP instead, so any of those can reach it over the API. A static public IP is recommended if you are going over the internet, with a firewall rule that allows only the addresses you expect. Server mode changes where the solver listens and nothing else: it is still your hardware, and it is still unmetered. Read the host from an environment variable so one build works in both places. The client does not read CAPSKIP_HOST by itself, so pass it to the constructor, as the full example below does.
| Where the C# runs | Which connection mode |
|---|---|
| On the CapSkip machine, in an IDE or a console app | Local mode. 127.0.0.1 is genuinely correct |
| On another box on the same network | Server mode, on that machine’s private address |
| On a container host, VPS or managed platform | Server mode with a static public IP and a firewall rule |
One ALTCHA-specific note on proxies. A proxy is supported here, but it is used only for the challenge fetch. There is no browser session to route, so it has no effect on the proof of work itself.
Full working example
// dotnet add package CapSkip
using CapSkip;
var http = new HttpClient();
var solver = new CapSkipClient(
host: Environment.GetEnvironmentVariable("CAPSKIP_HOST") ?? "127.0.0.1",
port: 8080);
try
{
var result = await solver.AltchaAsync(
"https://example.com/signup",
new Dictionary<string, object?>
{
["challenge_url"] = "https://example.com/altcha/challenge",
});
// Submit here, while the challenge is still fresh.
var body = new FormUrlEncodedContent(new Dictionary<string, string>
{
["email"] = "[email protected]",
["altcha"] = result.Token!,
});
var response = await http.PostAsync("https://example.com/signup", body);
Console.WriteLine($"{(int)response.StatusCode} after counter {result.Number}");
}
catch (ApiException ex)
{
// ERROR_CAPTCHA_UNSOLVABLE here means Argon2id or scrypt.
Console.WriteLine($"refused: {ex.Message}");
}
catch (CapSkip.TimeoutException)
{
Console.WriteLine("gave up waiting; defaultTimeout is 120 seconds");
}The other types are the same shape with a different method. RecaptchaAsync takes a sitekey and a page URL, TurnstileAsync and GeetestAsync work the same way, and image solving is a base64 call. The full method list is on the C# CAPTCHA solver page.
Challenge-page Turnstile is the one type that needs more than a sitekey. Its extra values are covered in the C# challenge-page guide.
Common errors and what they mean
| What you see | Cause | Fix |
|---|---|---|
| A bare verification failure from the site, with a token that looks fine | The challenge expired before the form was submitted | Fetch, solve and submit in one unit of work |
| ERROR_CAPTCHA_UNSOLVABLE inside an ApiException, in about a third of a second | The challenge uses Argon2id or scrypt | Nothing to retry. Those two are refused by design |
| A ValidationException on the call | Neither challenge option was supplied, or an option was passed that ALTCHA does not take | Pass the challenge endpoint or the challenge document, and drop anything else |
| A NetworkException on the first solve | CapSkip is not running, or the host and port are wrong | Start CapSkip, then check whether it should be in Local mode or Server mode |
| The Token property on the result reads null | Token is populated for ALTCHA only | Call AltchaAsync. On an ALTCHA result the Code property holds the same string |
| The build fails on an ambiguous TimeoutException | CapSkip and System both define that short name | Write CapSkip.TimeoutException in full, or catch CapSkipError |
| The form rejects a token your logs show was solved | Something re-encoded, trimmed or reordered the payload | Pass the string straight through, untouched |
FAQ
Does solving ALTCHA in C# need a browser?
No, and that is the useful part. ALTCHA hands out a hashing problem rather than something to look at, so the work is CPU only and finishes in milliseconds. You need no WebDriver, no headless Chrome and no user agent. A console app with an HttpClient is enough, which also means it runs happily inside a worker service, a queue consumer or a build step where driving a browser would be awkward.
Can a .NET app on a hosted platform reach the solver?
Yes. Switch CapSkip to Server mode under connection settings so it listens on a network address instead of loopback, then point CAPSKIP_HOST at that address. A container host, a VPS, a CI agent or a managed app service all connect the same way, over the same HTTP API. Use a static public IP if the route goes over the internet, and restrict it with a firewall rule. The solver stays on hardware you own in every one of those cases, so nothing about the licence or the solve count changes.
Should I pass the endpoint or the challenge document?
Pass the endpoint unless you already have the document. It is one entry in the options dictionary, it saves you a request, and if the challenge goes stale while the job is queued the solver fetches a fresh one by itself. Pass the document when your scraper already read it off the page, when the challenge is embedded in the HTML rather than served from an endpoint, or when fetching it needs cookies or headers your code has and the solver does not. In that last case the proxy option is worth knowing about, since for ALTCHA it applies to the fetch and only the fetch.
Why is my counter a different number every time?
Because it is the answer to that particular challenge, not a property of the site. Each challenge carries its own salt, so the number that satisfies it changes on every issue, and it can land anywhere up to the maxnumber the challenge allows. A large counter simply means more hashing was needed, which shows up as a few extra milliseconds and nothing else. It is useful in logs as evidence the work was really done, and useless as something to cache.
The short version
Read the challenge endpoint off the widget, pass it to the one ALTCHA method with the page URL, and post the token back into the field named altcha without touching it. Keep the fetch, the solve and the submit in the same block, because the challenge window can close inside two minutes and an expired challenge looks exactly like a wrong answer. Expect ERROR_CAPTCHA_UNSOLVABLE only from Argon2id and scrypt, which are refused outright rather than attempted. Switch to Server mode the moment the calling code stops sharing a machine with the solver.
- What the challenge is and how the type works: the ALTCHA solver page.
- Every other method the .NET package exposes: the C# and .NET solver page.
One last thing that changes how you design the retry. Because a local captcha solver computes the proof of work on a machine you already own, retrying an expired challenge costs a few milliseconds of your own CPU and nothing else, so you can afford to fetch a fresh challenge rather than nursing a stale one.
