How to Solve Cloudflare Turnstile in Node.js and TypeScript

Cloudflare Turnstile shows up in two forms and they need different code. A widget inside a form is a two-argument call. A full-page interstitial challenge needs two extra values scraped from the page, and the token will only be accepted if you send back the user agent the solver used. That last requirement is the one that produces tokens which look perfect and fail every time.
Telling them apart
| Widget | Challenge page | |
|---|---|---|
| Appearance | A checkbox inside a usable form | Full-page interstitial, page blocked |
| Needs cData and chlPageData | No | Yes |
| Needs the returned user agent | No | Yes |
Our live Turnstile demo runs the widget variant, which is a useful reference when you are working out which one you are looking at.
Setup
npm install capskip
const { CapSkip } = require('capskip');
const solver = new CapSkip({
host: '127.0.0.1',
port: 8080,
recaptchaTimeout: 300, // seconds, also covers Turnstile
});CapSkip solves locally, so the desktop app has to be running before any call succeeds.
Widget mode
const result = await solver.turnstile( '0x4AAAAAAA...', // the data-sitekey attribute 'https://example.com/login', ); console.log(result.code); // cf-turnstile-response token
Put result.code into the cf-turnstile-response field and submit. Nothing else is needed.
Challenge pages need two more values
An interstitial carries per-request state that the token gets bound to. Two parts of it have to travel with the solve:
- cData, passed as
data - chlPageData, passed as
pagedata
Both live in the challenge page itself rather than in a form attribute, so the page has to be fetched before it can be solved. On a standard Cloudflare interstitial they sit on the page’s own challenge options object, next to the sitekey. They are single use and tied to that page load, so fetch and solve together rather than caching.
const result = await solver.turnstile(sitekey, pageUrl, {
data: cData, // the cData value
pagedata: chlPageData, // the chlPageData value
action: 'managed', // optional, when the page declares one
});
console.log(result.code);
console.log(result.userAgent); // required for the next stepThe user agent is not optional
Turnstile ties the token to the browser fingerprint that produced it, and the user agent is part of that fingerprint. CapSkip returns the one it used in result.userAgent. Submit the token with Node’s default fetch user agent instead and Cloudflare rejects an otherwise perfectly valid token.
userAgent is populated for Turnstile only. It is undefined for every other CAPTCHA type, which is exactly why this catches people who reuse a working reCAPTCHA helper.
const response = await fetch(pageUrl, {
method: 'POST',
headers: {
// Send back the exact user agent the solve was performed with.
'User-Agent': result.userAgent,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
'cf-turnstile-response': result.code,
}),
});If a token is being rejected and the cData is fresh, this is almost always the reason.
TypeScript
import { CapSkip, SolveResult } from 'capskip';
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });
const result: SolveResult = await solver.turnstile(sitekey, pageUrl);
// userAgent is optional on the type, because only Turnstile populates it.
if (result.userAgent) {
// safe to forward
}Definitions ship with the package, so there is no @types install. The optional typing on userAgent is a useful nudge: the compiler will not let you forget that other CAPTCHA types leave it empty.
Proxies and concurrency
// Solve through the same egress you will submit from.
await solver.turnstile(sitekey, pageUrl, {
data: cData,
pagedata: chlPageData,
proxy: { type: 'HTTPS', uri: 'user:[email protected]:3128' },
});
// Several at once.
const results = await Promise.all(targets.map((t) =>
solver.turnstile(t.sitekey, t.url)));Proxies work for Turnstile, reCAPTCHA and GeeTest, but not for image CAPTCHAs, which never touch the target site. For challenge pages, each solve needs its own freshly fetched cData, so fetch inside the mapped function rather than in a batch beforehand.
Frequently asked questions
Do widgets ever need cData?
No, and passing empty values will make the solve fail rather than help. Only full-page interstitial challenges use them.
The token looks fine but the site rejects it.
Almost always the user agent. Submit using result.userAgent rather than whatever your HTTP client sends by default. The second most likely cause is a stale cData, which is bound to a single page load.
Can I use this with Puppeteer?
Yes, and it pairs well: read cData from the loaded page with page.evaluate, solve with this SDK, then set the same user agent on the page before continuing. Our Node.js CAPTCHA solver page covers the automation side.
Summary
Widgets take a sitekey and a URL. Challenge pages need data and pagedata read fresh from the page, and the token must be submitted with result.userAgent. Everything else is ordinary Promise handling.
Other languages are on the Cloudflare Turnstile solver page, parameter details in the API documentation, and the wider Node surface on the Node.js CAPTCHA solver page. CapSkip is an unlimited captcha solver that runs on your own hardware.
