{"id":26199,"date":"2026-10-02T12:15:25","date_gmt":"2026-10-02T12:15:25","guid":{"rendered":"https:\/\/capskip.com\/?p=26199"},"modified":"2026-10-02T12:15:25","modified_gmt":"2026-10-02T12:15:25","slug":"captchafox-nodejs","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/captchafox-nodejs\/","title":{"rendered":"\u5982\u4f55\u5728 Node.js \u4e2d\u7528\u6b63\u786e\u7684 user agent \u8bc6\u522b CaptchaFox"},"content":{"rendered":"<p>To solve CaptchaFox in Node.js, call the captchafox method of the capskip package with the sitekey and the page URL, then post the token it returns in the cf-captcha-response field. Send that post with three things fetch will not add for you: the cookies the page set, a Referer, and the user agent CapSkip returned next to the token. The user agent is the trap most Node code falls into. Node&#8217;s built-in fetch tells every server that its user agent is node, and a site that expects the token from the browser that minted it turns that post down, however good the solve was. CapSkip added CaptchaFox in version 1.4.0. This guide covers reading the key, the call, the submit, retries and running many sign-ups at once.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>CapSkip 1.4.0 or later running on a Windows machine. That release added CaptchaFox, together with Friendly Captcha and Capy Puzzle.<\/li>\n<li>Node.js 20.18.1 or later, the oldest release cheerio supports. On a new project use 22 or 24, because Node 20 reached end of life in April 2026.<\/li>\n<li>Version 1.3.0 or later of the capskip package, the first release with a captchafox method, plus cheerio to read the HTML.<\/li>\n<li>The URL of the page that shows the widget. The sitekey, the form fields and the widget build all come out of that page.<\/li>\n<li>An address for the solver. In Local mode CapSkip answers on 127.0.0.1 for that device only; in Server mode it listens on your network address or public IP, so a script on another machine can call it over the API. Both are set under <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>.<\/li>\n<\/ul>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># capskip 1.3.0 added captchafox(); cheerio reads the HTML\nnpm install capskip cheerio<\/pre>\n<\/div>\n<p>The samples are ES modules with top-level await. Save them with an .mjs extension, or set the type field in your package.json to module, since npm init now writes commonjs there. The capskip package also loads with require if your project is CommonJS.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: fetch the page, keep its cookies and read the sitekey<\/h2>\n<p>Node&#8217;s fetch keeps no cookies between calls, so the session cookie the page sets is gone by the time you post the form. A site that ties its CSRF token to that session then rejects the post. Carry the cookies yourself with Headers.getSetCookie(), which returns each Set-Cookie line on its own. The <a href=\"https:\/\/capskip.com\/friendly-captcha-nodejs\/\">guide to solving Friendly Captcha in Node.js<\/a> explains this small jar in more detail.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ npm install capskip cheerio\nimport * as cheerio from &quot;cheerio&quot;;\n\nconst PAGE_URL = &quot;https:\/\/example.com\/signup&quot;;\n\n\/\/ fetch() keeps no cookies between calls, so carry the page's own.\nconst jar = new Map();\nconst page = await fetch(PAGE_URL);\nfor (const line of page.headers.getSetCookie()) {\n  const pair = line.split(&quot;;&quot;)[0];\n  const eq = pair.indexOf(&quot;=&quot;);\n  jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());\n}\nconst html = await page.text();\nconst $ = cheerio.load(html);<\/pre>\n<\/div>\n<p>The sitekey is public, identical for every visitor, and conventionally starts with sk_. Most pages put it on the widget&#8217;s container, a div with the class captchafox and a data-sitekey attribute. A page that builds the widget from its own script passes the key to a captchafox.render call instead. When neither is in the HTML you fetched, open DevTools: the key is the path segment after \/captcha\/ in a request to api.captchafox.com, or to mam-api.captchafox.com on MAM pages.<\/p>\n<p>Check which build loads the widget while you are there. Most sites load it from cdn.captchafox.com and expect a plain token. Some platforms embed a package from s.uicdn.com\/mampkg\/ instead, and those sites expect a token that starts with MAM_. A MAM page may carry the key in that script&#8217;s own address, after ?key=. The same CDN serves unrelated packages too, so match the captchafox package path rather than the host.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ The container, matched on the whole class name.\nconst widget = $(&quot;.captchafox&quot;).first();\n\/\/ MAM pages may carry the key in the package's script src instead.\nconst mamSrc = $('script[src*=&quot;mampkg\/@mamdev\/core.frontend.libs.captchafox&quot;]').attr(&quot;src&quot;);\nconst sitekey = widget.attr(&quot;data-sitekey&quot;)\n  ?? (mamSrc &amp;&amp; new URL(mamSrc, PAGE_URL).searchParams.get(&quot;key&quot;))\n  ?? html.match(\/captchafox\\.render\\([\\s\\S]{0,300}?sitekey\\s*:\\s*[&quot;']([^&quot;']+)\/)?.[1];\nconst form = widget.length ? widget.closest(&quot;form&quot;) : $(&quot;form&quot;).first();\n\nconsole.log(sitekey, Boolean(mamSrc));   \/\/ a MAM page needs a MAM_ token<\/pre>\n<\/div>\n<p>cheerio&#8217;s class selector matches whole class names, so a wrapper element with a class such as captchafox-wrapper is not picked up by accident. The last fallback reads a key passed inline to the render call. A key that lives in a bundled script file is not in the HTML at all, and DevTools is the way to find it. Without a container, the code takes the first form on the page. If the page also has a search box or a second form, select the sign-up form by its id or its action instead.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: the captchafox call<\/h2>\n<p>To solve CaptchaFox in Node.js you need one method. It takes the sitekey, the page URL and an optional options object, and for the standard widget the first two are all it needs.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">import { CapSkip } from &quot;capskip&quot;;\n\nconst solver = new CapSkip({ host: &quot;127.0.0.1&quot;, port: 8080 });\n\/\/ Copied from the script tag on MAM pages.\nconst MAM_PACKAGE = &quot;https:\/\/s.uicdn.com\/mampkg\/@mamdev\/core.frontend.libs.captchafox\/&quot;;\n\nconst result = await solver.captchafox(sitekey, PAGE_URL, {\n  \/\/ undefined is dropped, so the standard widget sends nothing extra.\n  apiServer: mamSrc ? MAM_PACKAGE : undefined,\n});\n\nconsole.log(result.token.slice(0, 24));   \/\/ goes in cf-captcha-response\nconsole.log(result.userAgent);            \/\/ the browser that minted it<\/pre>\n<\/div>\n<p>result.token and result.code hold the same string, and result.captchaId is CapSkip&#8217;s id for the job. result.userAgent is the identity of the browser that minted the token, which is CapSkip&#8217;s own browser and not anything you sent. It is only set when the solve reported one, so test it before you use it.<\/p>\n<p>The page URL has to be the page the widget really runs on. CaptchaFox registers every key against a list of allowed domains and checks the host before it issues anything. A page URL on any other domain is refused on every attempt, so a shortened link, a search result or a redirect from another domain will not do.<\/p>\n<h3 style=\"font-size:1.3rem;line-height:1.4;\">Options the method accepts<\/h3>\n<p>apiServer is an alias of api_server, the widget build to solve. The method drops any option whose value is undefined or null, which is why the line above can pass it on every call. It also takes a proxy object with type and uri keys, and useragent, which is accepted and deliberately not applied, because CapSkip solves in a real browser with its own identity. A per-call timeout in seconds replaces the client&#8217;s recaptchaTimeout for that one solve. The poll ceiling can be set per call too, but only as polling_interval: the camelCase pollingInterval belongs to the constructor, and passed per call it throws ValidationException. An empty sitekey or URL, an unknown option and a proxy type other than HTTP, HTTPS, SOCKS5 or SOCKS5H throw ValidationException too, before any request leaves your machine.<\/p>\n<p>In TypeScript, the bundled types still describe token as optional and meant for ALTCHA, so strict mode needs a non-null assertion on result.token. The value is there for every CaptchaFox solve.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: submit under the user agent that minted the token<\/h2>\n<p>When you solve CaptchaFox in Node.js, this is the step that decides whether the site takes the token. Out of the box, Node&#8217;s fetch sends a User-Agent header with the single word node in it, on Node 20 and on Node 25 alike. That is not the browser that produced the token, so copy the returned value onto the request that carries it:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ The form's own fields, hidden CSRF token included.\nconst body = new URLSearchParams(\n  form.serializeArray().map((f) =&gt; [f.name, f.value]),\n);\nbody.set(&quot;email&quot;, &quot;YOUR_EMAIL&quot;);\nbody.set(&quot;cf-captcha-response&quot;, result.token);\n\nconst headers = {\n  cookie: [...jar].map(([k, v]) =&gt; `${k}=${v}`).join(&quot;; &quot;),\n  \/\/ Browsers send the page as Referer; strict servers want one.\n  referer: PAGE_URL,\n};\n\/\/ Without this line, fetch announces itself as user-agent: node.\nif (result.userAgent) headers[&quot;user-agent&quot;] = result.userAgent;\n\nconst res = await fetch(new URL(form.attr(&quot;action&quot;) || PAGE_URL, PAGE_URL), {\n  method: &quot;POST&quot;,\n  headers,\n  body,\n});\nconsole.log(res.status);<\/pre>\n<\/div>\n<p>serializeArray collects the fields a browser would send, the hidden CSRF token and any ticked checkbox included, so you never have to name them. It leaves out the submit button, so if the server checks for the button&#8217;s name, add it with body.set. The Referer matters as much as the cookie. fetch sends no Referer and no Origin, and some frameworks, Django among them, refuse an HTTPS form post that has neither, even with the right cookie and token.<\/p>\n<p>Two more rules come from how CaptchaFox checks the token on the site&#8217;s side. Each token verifies once and only for a short time, so solve when you are ready and submit straight away. And treat the token as opaque: never trim or re-encode it. Some sites send it in a JSON body from their own script, so submit the form once by hand with DevTools open and mirror what the page sends. CaptchaFox&#8217;s <a href=\"https:\/\/docs.captchafox.com\/en\/getting-started\" rel=\"nofollow noopener\" target=\"_blank\">getting started documentation<\/a> shows the widget markup and the server-side check.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 4: retries, challenge types and timeouts<\/h2>\n<p>You do not choose which challenge CaptchaFox draws. Most solves draw nothing at all, because the browser evidence clears on its own, and when a slide puzzle appears CapSkip usually completes it. Image-select and audio challenges are rare and are not solved: they come back as an ApiException whose message contains ERROR_CAPTCHA_UNSOLVABLE, within seconds rather than at the end of the timeout. A fresh attempt usually draws something else, so retrying is worth it, either in CapSkip or in your code.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">import { ApiException } from &quot;capskip&quot;;\n\nasync function solveCaptchaFox(sitekey, url, options = {}, attempts = 3) {\n  for (let attempt = 1; ; attempt++) {\n    try {\n      return await solver.captchafox(sitekey, url, options);\n    } catch (err) {\n      \/\/ Select, audio and unreadable slide draws come back unsolvable;\n      \/\/ redraw. Anything else, such as a wrong API key, is final.\n      const redraw = err instanceof ApiException &amp;&amp; err.message.includes(&quot;UNSOLVABLE&quot;);\n      if (!redraw || attempt === attempts) throw err;\n    }\n  }\n}<\/pre>\n<\/div>\n<p>The ApiException message is the raw reply from CapSkip, so the check reads the error code straight out of it. One catch: a key the site refuses, for example on the wrong domain, also reads as unsolvable, so it gets retried too. It fails within seconds on every attempt and never succeeds, which is the tell. The other route is the Retries setting in the CaptchaFox row of CapSkip&#8217;s settings. It takes 0 to 3 and is 0 by default. CapSkip then retries unsupported challenges, timed-out attempts and browser errors on its own, but never a refused key.<\/p>\n<p>captchafox polls on the client&#8217;s recaptchaTimeout, 300 seconds by default, because it is a real browser session. CapSkip runs its own clock as well: a task can wait up to 250 seconds (Wait Timeout) for one of the 10 CaptchaFox threads (Max. Threads), and a single attempt gets 150 seconds (Row Timeout). With a thread free, a failed attempt reaches you as an ApiException well inside the SDK&#8217;s 300 seconds. After a long queue, the SDK can hit its limit first and throw TimeoutException while CapSkip is still working. So pass a longer timeout when you queue many solves or raise Retries, since each retry can add another Row Timeout. The SDK also polls straight through connection errors, so if CapSkip stops mid-solve the call ends in TimeoutException, not NetworkException.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Full working example<\/h2>\n<p>Here is the whole run in one file. signUp fetches the page, reads the key, solves with retries and posts the form with its cookies, a Referer and the minting user agent.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ npm install capskip cheerio\n\/\/ signup.mjs: run with node signup.mjs\nimport * as cheerio from &quot;cheerio&quot;;\nimport {\n  CapSkip, ApiException, CapSkipError, TimeoutException, ValidationException,\n} from &quot;capskip&quot;;\n\nconst PAGE_URL = &quot;https:\/\/example.com\/signup&quot;;\n\/\/ Copied from the script tag on MAM pages.\nconst MAM_PACKAGE = &quot;https:\/\/s.uicdn.com\/mampkg\/@mamdev\/core.frontend.libs.captchafox\/&quot;;\n\nconst solver = new CapSkip({\n  apiKey: process.env.CAPSKIP_API_KEY ?? &quot;capskip&quot;,\n  host: process.env.CAPSKIP_HOST ?? &quot;127.0.0.1&quot;,\n  port: Number(process.env.CAPSKIP_PORT ?? 8080),\n});\n\nasync function solveCaptchaFox(sitekey, url, options = {}, attempts = 3) {\n  for (let attempt = 1; ; attempt++) {\n    try {\n      return await solver.captchafox(sitekey, url, options);\n    } catch (err) {\n      \/\/ Select, audio and unreadable slide draws come back unsolvable.\n      const redraw = err instanceof ApiException &amp;&amp; err.message.includes(&quot;UNSOLVABLE&quot;);\n      if (!redraw || attempt === attempts) throw err;\n      console.log(`attempt ${attempt}: ${err.message}`);\n    }\n  }\n}\n\nasync function signUp(pageUrl, email) {\n  \/\/ One jar per run, so runs side by side never share a session.\n  const jar = new Map();\n  const page = await fetch(pageUrl);\n  for (const line of page.headers.getSetCookie()) {\n    const pair = line.split(&quot;;&quot;)[0];\n    const eq = pair.indexOf(&quot;=&quot;);\n    jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());\n  }\n  const html = await page.text();\n  const $ = cheerio.load(html);\n\n  const widget = $(&quot;.captchafox&quot;).first();\n  const mamSrc = $('script[src*=&quot;mampkg\/@mamdev\/core.frontend.libs.captchafox&quot;]').attr(&quot;src&quot;);\n  const sitekey = widget.attr(&quot;data-sitekey&quot;)\n    ?? (mamSrc &amp;&amp; new URL(mamSrc, pageUrl).searchParams.get(&quot;key&quot;))\n    ?? html.match(\/captchafox\\.render\\([\\s\\S]{0,300}?sitekey\\s*:\\s*[&quot;']([^&quot;']+)\/)?.[1];\n  if (!sitekey) throw new Error(&quot;No sitekey in the HTML; find it in DevTools.&quot;);\n  const form = widget.length ? widget.closest(&quot;form&quot;) : $(&quot;form&quot;).first();\n\n  const result = await solveCaptchaFox(sitekey, pageUrl, {\n    apiServer: mamSrc ? MAM_PACKAGE : undefined,\n  });\n\n  const body = new URLSearchParams(form.serializeArray().map((f) =&gt; [f.name, f.value]));\n  body.set(&quot;email&quot;, email);\n  body.set(&quot;cf-captcha-response&quot;, result.token);\n  const headers = {\n    cookie: [...jar].map(([k, v]) =&gt; `${k}=${v}`).join(&quot;; &quot;),\n    referer: pageUrl,\n  };\n  if (result.userAgent) headers[&quot;user-agent&quot;] = result.userAgent;\n\n  const action = new URL(form.attr(&quot;action&quot;) || pageUrl, pageUrl);\n  return fetch(action, { method: &quot;POST&quot;, headers, body });\n}\n\ntry {\n  const res = await signUp(PAGE_URL, &quot;YOUR_EMAIL&quot;);\n  console.log(res.status, (await res.text()).slice(0, 80));\n} catch (err) {\n  if (err instanceof ValidationException) console.error(`not sent: ${err.message}`);\n  else if (err instanceof TimeoutException) console.error(&quot;gave up after recaptchaTimeout&quot;);\n  else if (err instanceof CapSkipError) console.error(`solve failed: ${err.name}: ${err.message}`);\n  else throw err;\n  process.exitCode = 1;\n}<\/pre>\n<\/div>\n<p>If every run fails within seconds, check the page URL before anything else, because a key used outside its registered domains fails the same way each time. If the sitekey check throws, the page builds the widget from a bundled script, and the key is in the Network tab as described in Step 1. The raw endpoint behind this method is documented in the <a href=\"https:\/\/capskip.com\/api-docs\/#captchafox\">CaptchaFox section of the API reference<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Running many sign-ups side by side<\/h2>\n<p>Every CapSkip method in Node returns a Promise, and AsyncCapSkip is just another name for the same class, so running solves side by side is plain JavaScript. What you need is a cap. CapSkip runs 10 CaptchaFox solves at a time by default and makes the rest wait for a free thread, so a small pool that keeps ten in flight matches it. Put it in place of the try block at the end of the full example:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ At most `limit` calls in flight; a failure becomes that item's Error.\nasync function mapLimited(items, limit, fn) {\n  const results = new Array(items.length);\n  let next = 0;\n  async function worker() {\n    while (next &lt; items.length) {\n      const i = next++;\n      results[i] = await fn(items[i]).catch((err) =&gt; err);\n    }\n  }\n  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));\n  return results;\n}\n\nconst emails = [&quot;YOUR_EMAIL_1&quot;, &quot;YOUR_EMAIL_2&quot;, &quot;YOUR_EMAIL_3&quot;];\nconst results = await mapLimited(emails, 10, (email) =&gt; signUp(PAGE_URL, email));\nfor (const r of results) console.log(r instanceof Error ? r.message : r.status);<\/pre>\n<\/div>\n<p>signUp is the function from the full example. It builds its own cookie jar on every call, which is what makes it safe in parallel: one jar shared by ten runs would mix up their sessions and CSRF tokens. It also keeps each token next to its own user agent until the post goes out. Raise Max. Threads in CapSkip&#8217;s CaptchaFox settings for a bigger pool. CaptchaFox also scores the network as well as the browser, so many solves from one address raise the challenge rate over a long run. Set up proxies with the Configure Proxies button in that same row, or pass a proxy object on each call.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Running the solver on another machine<\/h2>\n<p>127.0.0.1 is right while the script and CapSkip share a Windows PC. When the Node code moves to a VPS, a container, a CI runner or a serverless function, loopback points at the wrong machine and the first solve throws NetworkException with ECONNREFUSED. Switch CapSkip to Server mode and it listens on your network address or public IP, so any of those can call it over the same API. Use a static public IP when the route crosses the internet, turn on API key validation, and limit the port to the addresses you expect with a Windows Firewall rule. The solver is still your own Windows machine, and solves are still unmetered.<\/p>\n<p>The client does not read environment variables by itself. Read CAPSKIP_HOST, CAPSKIP_PORT and CAPSKIP_API_KEY in your code and pass them to the constructor, as the full example does, so the same script runs at your desk and on a server.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Common errors and what they mean<\/h2>\n<table>\n<thead>\n<tr>\n<th>What you see<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>The site rejects a token CapSkip returned as solved<\/td>\n<td>The post went out with user-agent: node, fetch&#8217;s default, not as the browser that minted the token<\/td>\n<td>Send result.userAgent as the User-Agent header on that request<\/td>\n<\/tr>\n<tr>\n<td>Rejected although the user agent matches<\/td>\n<td>The page loads the MAM package and the solve used the default widget, or the reverse<\/td>\n<td>Read the script tag and set apiServer to match it<\/td>\n<\/tr>\n<tr>\n<td>A 403 on the post, with a valid token and user agent<\/td>\n<td>The session cookie was not sent, so the CSRF field no longer matches, or the post had no Referer<\/td>\n<td>Carry the cookies from the page fetch and send the page URL as Referer<\/td>\n<\/tr>\n<tr>\n<td>result.userAgent is undefined<\/td>\n<td>The solve reported no user agent, so fetch sends its own<\/td>\n<td>Keep the if check; if the site refuses node, send a current desktop browser user agent yourself<\/td>\n<\/tr>\n<tr>\n<td>An ApiException within seconds, every time, for one key<\/td>\n<td>The page URL is outside the domains the key is registered for, or the key itself is wrong<\/td>\n<td>Send the page the widget runs on, not a redirect or a search result, and recheck the key<\/td>\n<\/tr>\n<tr>\n<td>An occasional ApiException containing ERROR_CAPTCHA_UNSOLVABLE<\/td>\n<td>CaptchaFox drew an image-select or audio challenge, or a slide CapSkip could not read<\/td>\n<td>Retry in code, or raise Retries in CapSkip&#8217;s CaptchaFox settings<\/td>\n<\/tr>\n<tr>\n<td>A token that worked once fails the second time<\/td>\n<td>Each token verifies once and expires quickly<\/td>\n<td>Solve a fresh token for every post, and post straight away<\/td>\n<\/tr>\n<tr>\n<td>ValidationException before anything is sent<\/td>\n<td>An empty sitekey or URL, an option the method does not take, such as a per-call pollingInterval, or a proxy type CapSkip does not accept<\/td>\n<td>Check the sitekey was found, and fix or drop the option the message names<\/td>\n<\/tr>\n<tr>\n<td>TimeoutException after five minutes<\/td>\n<td>The wait for a free CaptchaFox thread plus the solve itself ran past the SDK&#8217;s 300 seconds, or CapSkip stopped mid-solve<\/td>\n<td>Cap the pool at Max. Threads, or pass a longer timeout for that call<\/td>\n<\/tr>\n<tr>\n<td>NetworkException with ECONNREFUSED or ETIMEDOUT on the first call<\/td>\n<td>CapSkip is not running, the host or port is wrong, or a firewall drops the connection<\/td>\n<td>Start CapSkip, then check whether it should be in Local mode or Server mode<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">FAQ<\/h2>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Do axios or got avoid the user agent problem?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No. They only change the name. axios sends axios followed by its version number, and got sends got followed by the address of its GitHub repository. Neither keeps cookies or sends a Referer unless you set them up to, so the same three headers apply. Whatever the client, set the user agent on the request that carries the token, from result.userAgent.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can I use the token in Puppeteer or Playwright instead?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes, as long as the page that submits it runs under the user agent CapSkip returned. Solve first, then open a Playwright context with browser.newContext({ userAgent: ua }), or call page.setUserAgent(ua) in Puppeteer before the page loads. Load the form, write the token into the cf-captcha-response field and submit. If your automation does not need a browser for anything else, the fetch route above is lighter.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">How is this different from the Python version?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">The call and the rules are the same, and so is the main trap: the HTTP client sends its own name as the user agent. In Python that name is python-requests or python-httpx, and a requests.Session keeps the cookies for you. In Node it is node, and you carry the cookies yourself. The Python side is covered in the <a href=\"https:\/\/capskip.com\/captchafox-python\/\">guide to solving CaptchaFox in Python<\/a>.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>To solve CaptchaFox in Node.js, fetch the page and keep its cookies, read the sitekey from the captchafox container, the render call or the MAM script, and note which build the page loads. Call solver.captchafox() with the sitekey and the real page URL, adding apiServer only for MAM pages. Post result.token in cf-captcha-response with the page&#8217;s cookies, the page as Referer and result.userAgent as the User-Agent header, once and straight away. Retry unsolvable draws, keep no more solves in flight than Max. Threads, 10 by default, and switch to Server mode when the script leaves the solver&#8217;s machine.<\/p>\n<ul>\n<li>How CaptchaFox works and what the solver covers: <a href=\"https:\/\/capskip.com\/captchafox-solver\/\">the CaptchaFox solver page<\/a>.<\/li>\n<li>Every CAPTCHA type the Node.js package handles: <a href=\"https:\/\/capskip.com\/nodejs-captcha-solver\/\">the Node.js CAPTCHA solver page<\/a>.<\/li>\n<\/ul>\n<p>A redrawn challenge is the only retry this type usually needs, and with an <a href=\"https:\/\/capskip.com\/\">unlimited captcha solver<\/a> on your own machine each redraw costs a few seconds rather than another billed solve.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Node \u7684 fetch \u4f1a\u544a\u8bc9\u6bcf\u4e2a\u670d\u52a1\u5668\u5b83\u7684 user agent \u662f node\uff0c\u8fd9\u6837\u63d0\u4ea4\u7684 CaptchaFox token \u53ef\u80fd\u4f1a\u88ab\u62d2\u7edd\u3002\u672c\u6587\u7ed9\u51fa\u5b8c\u6574\u7684 Node.js \u4ee3\u7801\uff1a\u7528 CapSkip \u8bc6\u522b\u5c0f\u7ec4\u4ef6\uff0c\u518d\u4ee5\u7b7e\u53d1\u8be5 token \u7684\u6d4f\u89c8\u5668\u8eab\u4efd\u63d0\u4ea4\u5b83\u3002<\/p>","protected":false},"author":1,"featured_media":26198,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Solve CaptchaFox in Node.js (fetch, cheerio) | CapSkip","rank_math_description":"To solve captchafox in node.js, call solver.captchafox(), then post the token in cf-captcha-response under the user agent it returns, not as node.","rank_math_focus_keyword":"solve captchafox in node.js","footnotes":""},"categories":[70],"tags":[],"class_list":["post-26199","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-captcha"],"_links":{"self":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/26199","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/comments?post=26199"}],"version-history":[{"count":2,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/26199\/revisions"}],"predecessor-version":[{"id":26204,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/26199\/revisions\/26204"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/26198"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=26199"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=26199"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=26199"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}