{"id":25202,"date":"2026-08-13T09:54:43","date_gmt":"2026-08-13T09:54:43","guid":{"rendered":"https:\/\/capskip.com\/?p=25202"},"modified":"2026-08-14T22:02:53","modified_gmt":"2026-08-14T22:02:53","slug":"solve-captcha-curl","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/solve-captcha-curl\/","title":{"rendered":"\u5982\u4f55\u7528 cURL \u548c\u539f\u59cb HTTP API \u8bc6\u522b\u9a8c\u8bc1\u7801"},"content":{"rendered":"<p>You do not need an SDK. The CapSkip API is 2captcha compatible, so you can solve captcha with curl in exactly two calls: POST the task to <code>\/in.php<\/code> and get an ID back, then poll <code>\/res.php<\/code> until the answer appears. Everything runs on <code>127.0.0.1:8080<\/code>, so there is no remote endpoint and nothing is billed per solve. This guide has the exact parameters for every CAPTCHA type, the JSON response shapes, and a script you can paste straight into a terminal.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>The CapSkip app running, with its local service started. Port and key settings are covered in the <a href=\"https:\/\/capskip.com\/setup-guide\/\">setup guide<\/a>.<\/li>\n<li><code>curl<\/code>. It ships with macOS, every Linux distribution, and Windows 10 build 1803 and later.<\/li>\n<li><code>jq<\/code> if you want to pull fields out of the JSON responses. Optional, but it makes the examples one-liners.<\/li>\n<\/ul>\n<p>Key validation is off by default, so any non-empty string works as <code>key<\/code>. Send something rather than nothing: an empty key returns <code>ERROR_WRONG_USER_KEY<\/code>.<\/p>\n<p>That is the whole dependency list, because the whole API is two endpoints:<\/p>\n<table>\n<thead>\n<tr>\n<th>Endpoint<\/th>\n<th>What it does<\/th>\n<th>Returns<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>\/in.php<\/code><\/td>\n<td>Submits a task<\/td>\n<td>A numeric captcha ID<\/td>\n<\/tr>\n<tr>\n<td><code>\/res.php<\/code><\/td>\n<td>Asks whether that ID is done<\/td>\n<td>The answer, or <code>CAPCHA_NOT_READY<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Both accept GET or POST. POST is the better habit, because a page URL with query parameters in it will quietly truncate a GET request at the first unencoded ampersand.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: submit the task<\/h2>\n<p>reCAPTCHA v2 is the shortest example. Two values identify the job: the sitekey from the page, and the URL of the page it sits on.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># No install step. curl is already on your machine.\ncurl -X POST http:\/\/127.0.0.1:8080\/in.php \\\n  -d &quot;key=YOUR_API_KEY&quot; \\\n  -d &quot;method=userrecaptcha&quot; \\\n  -d &quot;googlekey=YOUR_SITEKEY&quot; \\\n  -d &quot;pageurl=https:\/\/example.com\/page-with-recaptcha&quot;\n\nOK|2122988149   # the number after the pipe is your captcha ID<\/pre>\n<\/div>\n<p>Note the parameter name. reCAPTCHA uses <code>googlekey<\/code>. Turnstile uses <code>sitekey<\/code>. Sending <code>sitekey<\/code> to <code>userrecaptcha<\/code> is the single most common reason for an <code>ERROR_GOOGLEKEY<\/code> response.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: poll for the result<\/h2>\n<p>Wait, then ask. Polling immediately just burns a request and gets you <code>CAPCHA_NOT_READY<\/code>.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\">sleep 15\n\ncurl -X POST http:\/\/127.0.0.1:8080\/res.php \\\n  -d &quot;key=YOUR_API_KEY&quot; \\\n  -d &quot;action=get&quot; \\\n  -d &quot;id=2122988149&quot;\n\nOK|03AGdBq26...   # the token, ready to inject into the form<\/pre>\n<\/div>\n<p>Two things about <code>\/res.php<\/code> that catch people out. It returns <code>CAPCHA_NOT_READY<\/code> while the task is still running, which is not an error and means keep polling. And <strong>each result can be read only once<\/strong>, so store the answer the moment it arrives. A second read of the same ID comes back empty.<\/p>\n<p>How long to wait before the first poll depends on the type:<\/p>\n<table>\n<thead>\n<tr>\n<th>Type<\/th>\n<th>First poll after<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Image<\/td>\n<td>1 second<\/td>\n<\/tr>\n<tr>\n<td>reCAPTCHA v2<\/td>\n<td>15 to 20 seconds<\/td>\n<\/tr>\n<tr>\n<td>reCAPTCHA v3<\/td>\n<td>10 to 15 seconds<\/td>\n<\/tr>\n<tr>\n<td>GeeTest v3<\/td>\n<td>about 5 seconds<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3 style=\"font-size:1.3rem;line-height:1.4;\">Add json=1 so you can parse the reply<\/h3>\n<p>The plain text format is fine for a person reading a terminal and awkward for a script. Add <code>json=1<\/code> to either endpoint and you get a stable object instead.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"json\" class=\"EnlighterJSRAW\">\/\/ in.php with json=1\n{&quot;status&quot;: 1, &quot;request&quot;: &quot;2122988149&quot;}\n\n\/\/ res.php with json=1, once it is solved\n{&quot;status&quot;: 1, &quot;request&quot;: &quot;03AGdBq26...&quot;}\n\n\/\/ res.php with json=1, still working\n{&quot;status&quot;: 0, &quot;request&quot;: &quot;CAPCHA_NOT_READY&quot;}<\/pre>\n<\/div>\n<p><code>status<\/code> is 1 for success and 0 for everything else, and the interesting value is always in <code>request<\/code>. That makes the whole thing two <code>jq<\/code> expressions.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Every method in one table<\/h2>\n<p>Nine CAPTCHA types, five method values. Variants are extra parameters, not new endpoints.<\/p>\n<table>\n<thead>\n<tr>\n<th>Type<\/th>\n<th><code>method<\/code><\/th>\n<th>Required parameters<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Image, uploaded file<\/td>\n<td><code>post<\/code><\/td>\n<td><code>file<\/code><\/td>\n<\/tr>\n<tr>\n<td>Image, base64<\/td>\n<td><code>base64<\/code><\/td>\n<td><code>body<\/code><\/td>\n<\/tr>\n<tr>\n<td>reCAPTCHA v2<\/td>\n<td><code>userrecaptcha<\/code><\/td>\n<td><code>googlekey<\/code>, <code>pageurl<\/code><\/td>\n<\/tr>\n<tr>\n<td>reCAPTCHA v2 Invisible<\/td>\n<td><code>userrecaptcha<\/code><\/td>\n<td>plus <code>invisible=1<\/code><\/td>\n<\/tr>\n<tr>\n<td>reCAPTCHA Enterprise<\/td>\n<td><code>userrecaptcha<\/code><\/td>\n<td>plus <code>enterprise=1<\/code><\/td>\n<\/tr>\n<tr>\n<td>reCAPTCHA v3<\/td>\n<td><code>userrecaptcha<\/code><\/td>\n<td>plus <code>version=v3<\/code>, <code>action<\/code><\/td>\n<\/tr>\n<tr>\n<td>Turnstile widget<\/td>\n<td><code>turnstile<\/code><\/td>\n<td><code>sitekey<\/code>, <code>pageurl<\/code><\/td>\n<\/tr>\n<tr>\n<td>Turnstile challenge page<\/td>\n<td><code>turnstile<\/code><\/td>\n<td>plus <code>data<\/code>, <code>pagedata<\/code><\/td>\n<\/tr>\n<tr>\n<td>GeeTest v3<\/td>\n<td><code>geetest<\/code><\/td>\n<td><code>gt<\/code>, <code>challenge<\/code>, <code>pageurl<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>An image goes up as a form upload or as base64 in the body:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># File upload. Note the @ in front of the path. Use -F for every\n# field here: curl refuses to mix -F and -d in one request.\ncurl -X POST http:\/\/127.0.0.1:8080\/in.php \\\n  -F &quot;key=YOUR_API_KEY&quot; -F &quot;method=post&quot; -F &quot;file=@captcha.png&quot;\n\n# Or send the bytes inline, already base64 encoded.\ncurl -X POST http:\/\/127.0.0.1:8080\/in.php \\\n  -d &quot;key=YOUR_API_KEY&quot; \\\n  -d &quot;method=base64&quot; \\\n  --data-urlencode &quot;body=$(base64 &lt; captcha.png | tr -d '\\n')&quot;<\/pre>\n<\/div>\n<p>Use <code>--data-urlencode<\/code> for anything containing <code>+<\/code>, <code>\/<\/code> or <code>=<\/code>. Base64 payloads contain all three, and plain <code>-d<\/code> will mangle them.<\/p>\n<h3 style=\"font-size:1.3rem;line-height:1.4;\">Turnstile hands back a user agent as well<\/h3>\n<p>Cloudflare binds the token to the browser fingerprint that produced it, so submitting the token from a different user agent gets it rejected even though the token itself is valid. The raw API gives you the one that was used, in two places:<\/p>\n<ul>\n<li>With <code>json=1<\/code>, as a <code>userAgent<\/code> field on the response.<\/li>\n<li>In plain text mode, as the <code>X-Turnstile-User-Agent<\/code> response header.<\/li>\n<\/ul>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># -i prints the headers, which is where the user agent lives\n# when you are not using json=1.\ncurl -i -X POST http:\/\/127.0.0.1:8080\/res.php \\\n  -d &quot;key=YOUR_API_KEY&quot; -d &quot;action=get&quot; -d &quot;id=2122988149&quot;\n\nX-Turnstile-User-Agent: Mozilla\/5.0 ...\nOK|0.abc123...<\/pre>\n<\/div>\n<p>Full-page challenges also need <code>data<\/code> (the cData value) and <code>pagedata<\/code> (chlPageData) scraped from the page immediately before you submit. Widget mode needs neither. The <a href=\"https:\/\/capskip.com\/cloudflare-turnstile-solver\/\">Turnstile solver<\/a> page covers the difference in more detail.<\/p>\n<h3 style=\"font-size:1.3rem;line-height:1.4;\">GeeTest answers are three fields, not one<\/h3>\n<p>GeeTest does not return a single token. Ask for JSON and you get the three values the site&#8217;s own front end would post back.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"json\" class=\"EnlighterJSRAW\">{\n  &quot;status&quot;: 1,\n  &quot;request&quot;: {\n    &quot;geetest_challenge&quot;: &quot;...&quot;,\n    &quot;geetest_validate&quot;:  &quot;...&quot;,\n    &quot;geetest_seccode&quot;:   &quot;...&quot;\n  }\n}<\/pre>\n<\/div>\n<p>The <code>gt<\/code> value is static per site. The <code>challenge<\/code> value is single use and dies in about a minute, so fetch it immediately before you submit, never at the start of a long script.<\/p>\n<h3 style=\"font-size:1.3rem;line-height:1.4;\">Routing a solve through a proxy<\/h3>\n<p>Two parameters, added to the same <code>\/in.php<\/code> call:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\">curl -X POST http:\/\/127.0.0.1:8080\/in.php \\\n  -d &quot;key=YOUR_API_KEY&quot; \\\n  -d &quot;method=userrecaptcha&quot; \\\n  -d &quot;googlekey=YOUR_SITEKEY&quot; \\\n  -d &quot;pageurl=https:\/\/example.com\/page-with-recaptcha&quot; \\\n  -d &quot;proxy=login:password@1.2.3.4:3128&quot; \\\n  -d &quot;proxytype=HTTPS&quot;<\/pre>\n<\/div>\n<p><code>proxytype<\/code> takes HTTP, HTTPS, SOCKS5 or SOCKS5H. Proxies apply to reCAPTCHA, Turnstile and GeeTest only. Image solving reads pixels you already have and never touches the target site, so a proxy there does nothing.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">A complete script<\/h2>\n<p>Submit, poll with a ceiling, print the token. About twenty lines, no dependencies beyond <code>curl<\/code>.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\">#!\/usr\/bin\/env bash\nset -euo pipefail\n\nAPI=&quot;http:\/\/127.0.0.1:8080&quot;\nKEY=&quot;YOUR_API_KEY&quot;\n\n# Submit and keep only the part after the pipe.\nID=$(curl -s -X POST &quot;$API\/in.php&quot; \\\n  -d &quot;key=$KEY&quot; -d &quot;method=userrecaptcha&quot; \\\n  -d &quot;googlekey=YOUR_SITEKEY&quot; \\\n  -d &quot;pageurl=https:\/\/example.com\/page-with-recaptcha&quot; | cut -d'|' -f2)\n\nsleep 15\n\n# Poll every 5s, give up after 20 tries so this cannot hang forever.\nfor _ in $(seq 20); do\n  R=$(curl -s -X POST &quot;$API\/res.php&quot; -d &quot;key=$KEY&quot; -d &quot;action=get&quot; -d &quot;id=$ID&quot;)\n  [ &quot;$R&quot; = &quot;CAPCHA_NOT_READY&quot; ] || { echo &quot;${R#OK|}&quot;; exit 0; }\n  sleep 5\ndone\n\necho &quot;timed out waiting for $ID&quot; &gt;&amp;2; exit 1<\/pre>\n<\/div>\n<p>The <code>|| { ...; exit 0; }<\/code> branch fires on anything that is not <code>CAPCHA_NOT_READY<\/code>, which includes error codes. That is deliberate: an error means stop, not keep polling.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Errors you will meet at this layer<\/h2>\n<table>\n<thead>\n<tr>\n<th>Code<\/th>\n<th>Means<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>ERROR_WRONG_USER_KEY<\/code><\/td>\n<td>The key was missing or empty<\/td>\n<td>Send any non-empty <code>key<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>ERROR_WRONG_METHOD<\/code><\/td>\n<td>Bad <code>method<\/code> or <code>action<\/code><\/td>\n<td>Check the spelling against the table above<\/td>\n<\/tr>\n<tr>\n<td><code>ERROR_BAD_PARAMETERS<\/code><\/td>\n<td>A required parameter is missing<\/td>\n<td>Compare with the required column above<\/td>\n<\/tr>\n<tr>\n<td><code>ERROR_GOOGLEKEY<\/code><\/td>\n<td>The <code>googlekey<\/code> value was rejected<\/td>\n<td>You probably sent <code>sitekey<\/code> instead<\/td>\n<\/tr>\n<tr>\n<td><code>ERROR_PAGEURL<\/code><\/td>\n<td>The <code>pageurl<\/code> value was rejected<\/td>\n<td>Include the scheme, and POST rather than GET<\/td>\n<\/tr>\n<tr>\n<td><code>CAPCHA_NOT_READY<\/code><\/td>\n<td>Still working<\/td>\n<td>Not an error. Keep polling the same ID<\/td>\n<\/tr>\n<tr>\n<td>Empty response<\/td>\n<td>Already read, or no such ID<\/td>\n<td>Results are readable once. Store the first one<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The exact wording of every code the API can return is in the <a href=\"https:\/\/capskip.com\/api-docs\/\">API documentation<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">When to stop using curl<\/h2>\n<p>Raw HTTP is perfect for a smoke test, a shell pipeline, or a language with no official client. For application code the <a href=\"https:\/\/capskip.com\/captcha-solving-sdk\/\">CapSkip SDKs<\/a> are worth the dependency for one reason above all: they do not poll on a flat interval. They start at 250ms and back off to <code>pollingInterval<\/code>, so a solve typically returns sooner than the hand-rolled loop above, which sits out its full 15 second wait every time.<\/p>\n<p>They also turn the error strings into typed exceptions, and handle the Turnstile user agent and the GeeTest three-field answer for you. Official clients exist for Python, Node.js, PHP and .NET.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Frequently asked questions<\/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;\">Can I use GET instead of POST?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes, both endpoints accept it. The catch is that a <code>pageurl<\/code> containing its own query string will be cut off at the first unencoded ampersand, and you will get <code>ERROR_PAGEURL<\/code> or a solve against the wrong page. If you must use GET, run the URL through <code>--data-urlencode<\/code> first.<\/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;\">What API key should I send?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Any non-empty string. Key validation is off by default because the service listens on localhost only, so <code>key=capskip<\/code> works fine. It still has to be present: omit it and you get <code>ERROR_WRONG_USER_KEY<\/code> rather than a solve.<\/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;\">Does this work from Windows PowerShell?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Use <code>curl.exe<\/code> explicitly. In PowerShell, <code>curl<\/code> is an alias for <code>Invoke-WebRequest<\/code>, which does not understand <code>-d<\/code> and will throw a parameter error that looks like an API problem. Writing <code>curl.exe<\/code> bypasses the alias.<\/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 run several solves at once?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes. Submit as many tasks as you like and poll each ID independently. Nothing serialises them and there is no per-minute quota, because the work happens on your own hardware rather than in a shared queue.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Summary<\/h2>\n<p>POST the task to <code>\/in.php<\/code>, keep the ID, wait the type-appropriate delay, then poll <code>\/res.php<\/code> until you get something other than <code>CAPCHA_NOT_READY<\/code>. Add <code>json=1<\/code> if a script is reading the reply. Watch the two naming traps: <code>googlekey<\/code> for reCAPTCHA against <code>sitekey<\/code> for Turnstile, and the fact that a result can only be read once.<\/p>\n<p>Because it is a <a href=\"https:\/\/capskip.com\/\">captcha solver<\/a> running on your own machine, the loop above has no quota to respect and no balance to top up. Point it at localhost, and the only limit is how fast your CPU works through the queue.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u4e0d\u9700\u8981 SDK\uff0c\u4e5f\u6ca1\u6709\u4f9d\u8d56\u3002\u5bf9\u672c\u5730\u7684 2captcha \u517c\u5bb9 API \u53d1\u4e24\u6b21 curl \u8c03\u7528\uff1a\u63d0\u4ea4\u4efb\u52a1\uff0c\u8f6e\u8be2\u7b54\u6848\u3002\u6bcf\u4e2a method \u548c\u53c2\u6570\u90fd\u9f50\u5168\uff0c\u8fd8\u9644\u5e26\u4e00\u6bb5\u53ef\u7528\u811a\u672c\u3002<\/p>","protected":false},"author":1,"featured_media":25201,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Solve CAPTCHA With cURL: The Raw API | CapSkip","rank_math_description":"To solve captcha with curl you need two calls: POST in.php for a task ID, then poll res.php. Exact parameters, json=1 shapes and a full script inside.","rank_math_focus_keyword":"solve captcha with curl","footnotes":""},"categories":[70],"tags":[],"class_list":["post-25202","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\/25202","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=25202"}],"version-history":[{"count":2,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25202\/revisions"}],"predecessor-version":[{"id":25253,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25202\/revisions\/25253"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/25201"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=25202"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=25202"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=25202"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}