How to Solve Cloudflare Turnstile Challenge Pages in C#

Cloudflare Turnstile comes in two shapes, and the C# code for each is different. A widget embedded in a form needs nothing but a sitekey and a URL. A full-page interstitial challenge needs two extra values pulled out of the page, and it will only be accepted if you send back the user agent the solver used. Miss that last part and you get a token that looks perfect and fails validation every time.
This guide covers both, with working .NET code.
Widget or challenge page?
Work out which one you are looking at before writing any code.
| Widget mode | Challenge page | |
|---|---|---|
| What you see | A checkbox inside a form you can still interact with | A full-page interstitial, usually “Checking your browser” |
| Rest of the page | Loads normally | Blocked until the challenge clears |
Needs data / pagedata | No | Yes |
| Needs the returned user agent | No | Yes |
If you are unsure, our live Turnstile demo page runs the widget variant, so you can compare it against whatever you are actually hitting.
Setup
CapSkip runs on your own machine, so there is a local service to start before any of this works. Install the SDK from NuGet:
# .NET Standard 2.0, so this works on .NET Framework 4.6.1+, # .NET Core 2.0+, and .NET 6 through 9. dotnet add package CapSkip
Then point the client at the port shown in the CapSkip app:
using CapSkip;
var solver = new CapSkipClient(
apiKey: "capskip", // any string when key validation is off
host: "127.0.0.1",
port: 8080,
recaptchaTimeout: 300); // seconds, also covers Turnstile and GeeTestWidget mode, the easy case
Two arguments and you are done:
var result = await solver.TurnstileAsync(
"0x4AAAAAAA...", // sitekey from the data-sitekey attribute
"https://example.com/login");
Console.WriteLine(result.Code); // cf-turnstile-response tokenDrop result.Code into the cf-turnstile-response field and submit the form. Nothing else to do.
Challenge pages need two more values
An interstitial challenge carries per-request state that the token is bound to. Two pieces of it have to travel with your solve request:
- cData, passed as
data - chlPageData, passed as
pagedata
Both are embedded in the challenge page itself rather than in a form attribute, so you have to read the page before you can solve it. On a standard Cloudflare interstitial they are exposed on the page’s own challenge options object, alongside the sitekey.
They are also single-use and tied to that specific page load. Fetch them, solve immediately, and do not cache them between attempts.
using System.Collections.Generic;
using CapSkip;
var result = await solver.TurnstileAsync(
sitekey,
pageUrl,
new Dictionary<string, object?>
{
["data"] = cData, // the cData value from the page
["pagedata"] = chlPageData, // the chlPageData value
["action"] = "managed", // optional, when the page declares one
});
Console.WriteLine(result.Code);
Console.WriteLine(result.UserAgent); // you are going to need thisThe part everyone misses: the user agent
Turnstile binds the token to the browser fingerprint that produced it, and the user agent is part of that. CapSkip returns the one it used in result.UserAgent. If you then submit the token from an HttpClient sending its own default user agent, the values do not match and Cloudflare rejects a token that is otherwise completely valid.
UserAgent is populated for Turnstile only. It is null for every other CAPTCHA type, which is why this trips people up when they reuse a working reCAPTCHA helper.
using System.Net.Http;
using CapSkip;
var result = await solver.TurnstileAsync(sitekey, pageUrl, options);
using var http = new HttpClient();
// Send back the exact user agent the solve was performed with.
http.DefaultRequestHeaders.UserAgent.ParseAdd(result.UserAgent);
var form = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("cf-turnstile-response", result.Code),
});
var response = await http.PostAsync(pageUrl, form);If your token is being rejected and you have ruled out an expired cData, this is almost always the reason.
Handling failures properly
Every SDK exception derives from CapSkipError, so you can catch that one type or handle each individually. There is one .NET-specific trap worth knowing about.
using System;
using CapSkip;
try
{
var result = await solver.TurnstileAsync(sitekey, pageUrl, options);
}
catch (CapSkip.ValidationException) { /* bad parameters */ }
catch (NetworkException) { /* CapSkip is not running */ }
catch (ApiException) { /* API returned an error code */ }
catch (CapSkip.TimeoutException) { /* exceeded recaptchaTimeout */ }
catch (CapSkipError) { /* anything else from the SDK */ }TimeoutException and ValidationException exist in both System and CapSkip. With both namespaces imported, an unqualified catch (TimeoutException) resolves to the System one and silently never fires. Qualify them as above, or just catch CapSkipError and inspect it.
Solving several at once
Every method returns a Task, so ordinary .NET concurrency applies:
var results = await Task.WhenAll(
solver.TurnstileAsync(sitekeyA, "https://a.example.com"),
solver.TurnstileAsync(sitekeyB, "https://b.example.com"));
foreach (var r in results)
{
Console.WriteLine(r.Code);
}The SDK also exports AsyncCapSkip, but in .NET it is only an alias of CapSkipClient. It exists so code ported from the Python SDK keeps compiling. Switching to it gains you nothing, because .NET I/O is already asynchronous.
Routing through a proxy
If the challenge is geo-sensitive, solve from the same network path you will submit from:
var result = await solver.TurnstileAsync(sitekey, pageUrl,
new Dictionary<string, object?>
{
["data"] = cData,
["pagedata"] = chlPageData,
["proxy"] = new Proxy("HTTPS", "user:[email protected]:3128"),
});Proxies are supported for Turnstile, reCAPTCHA and GeeTest. They are not supported for image CAPTCHAs, which are solved from the image bytes and never touch the target site.
Frequently asked questions
Do I always need cData and chlPageData?
No. Only for full-page interstitial challenges. A Turnstile widget sitting inside a form needs nothing but the sitekey and the page URL, and passing empty values on a widget will make the solve fail rather than help.
My token is valid but the site still rejects it. Why?
Almost always the user agent. Turnstile ties the token to the fingerprint that produced it, so you have to submit using the value in result.UserAgent. The second most common cause is a stale cData, which is bound to a single page load.
Does this work on .NET Framework?
Yes. The package targets .NET Standard 2.0, so it runs on .NET Framework 4.6.1 and newer, .NET Core 2.0+, and every modern .NET release. From synchronous code you can call .GetAwaiter().GetResult(), though awaiting properly is better.
How long should a Turnstile solve take?
Usually a few seconds. The client polls internally, starting at 250ms and backing off to pollingInterval, and gives up at recaptchaTimeout which defaults to 300 seconds. If you are consistently hitting that ceiling, check that the CapSkip service is actually running rather than raising the timeout.
Summary
Widget Turnstile is a two-argument call. Challenge pages need data and pagedata read fresh from the page, and the token has to be submitted with the user agent returned alongside it. Catch CapSkipError rather than fighting the namespace collision, and use a proxy when the challenge is geo-sensitive.
The full method surface for .NET is on the C# CAPTCHA solver page, the parameter reference is in the API documentation, and Turnstile support covers the other languages. CapSkip itself is an unlimited captcha solver that runs locally, so none of the above costs per solve.
