{"id":24985,"date":"2026-08-06T10:17:56","date_gmt":"2026-08-06T10:17:56","guid":{"rendered":"https:\/\/capskip.com\/?p=24985"},"modified":"2026-08-07T08:33:22","modified_gmt":"2026-08-07T08:33:22","slug":"turnstile-php","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/turnstile-php\/","title":{"rendered":"\u5982\u4f55\u7528 PHP \u548c Composer \u8bc6\u522b Cloudflare Turnstile"},"content":{"rendered":"<p>Cloudflare Turnstile appears in two forms and they need different PHP. A widget sitting in a form is a two-argument call. A full-page interstitial challenge needs two more values read out of the page, and the token is only accepted if you send back the user agent the solver used. Skip that last part and you get a token that looks completely valid and is rejected every time.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Which one are you looking at?<\/h2>\n<table>\n<thead>\n<tr>\n<th><\/th>\n<th>Widget<\/th>\n<th>Challenge page<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Appearance<\/td>\n<td>A checkbox in a form you can still use<\/td>\n<td>Full-page interstitial, everything blocked<\/td>\n<\/tr>\n<tr>\n<td>Needs cData and chlPageData<\/td>\n<td>No<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td>Needs the returned user agent<\/td>\n<td>No<\/td>\n<td>Yes<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Our <a href=\"https:\/\/capskip.com\/captcha-demo\/cloudflare-turnstile\/\">live Turnstile demo<\/a> runs the widget variant, which is handy for comparison.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Setup<\/h2>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># PHP 8.0+, with the curl and json extensions.\ncomposer require capskip\/capskip<\/pre>\n<pre data-enlighter-language=\"php\" class=\"EnlighterJSRAW\">use CapSkip\\CapSkip;\n\n$solver = new CapSkip([\n    'host' =&gt; '127.0.0.1',\n    'port' =&gt; 8080,\n    'recaptchaTimeout' =&gt; 300,   \/\/ seconds, also covers Turnstile\n]);<\/pre>\n<p>CapSkip solves locally, so the desktop app has to be running before any call succeeds.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Widget mode<\/h2>\n<pre data-enlighter-language=\"php\" class=\"EnlighterJSRAW\">$result = $solver-&gt;turnstile(\n    '0x4AAAAAAA...',                 \/\/ the data-sitekey attribute\n    'https:\/\/example.com\/login'\n);\n\necho $result['code'];                \/\/ cf-turnstile-response token<\/pre>\n<p>Put <code>$result['code']<\/code> into the <code>cf-turnstile-response<\/code> field and submit the form. That is the whole flow for widgets.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Challenge pages need two more values<\/h2>\n<p>An interstitial carries per-request state that the token is bound to, and two parts of it have to travel with the solve:<\/p>\n<ul>\n<li><strong>cData<\/strong>, passed as <code>data<\/code><\/li>\n<li><strong>chlPageData<\/strong>, passed as <code>pagedata<\/code><\/li>\n<\/ul>\n<p>They live inside the challenge page 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&#8217;s own challenge options object alongside the sitekey. Both are single use and tied to that page load, so fetch and solve together rather than caching them.<\/p>\n<pre data-enlighter-language=\"php\" class=\"EnlighterJSRAW\">$result = $solver-&gt;turnstile($sitekey, $pageUrl, [\n    'data' =&gt; $cData,             \/\/ the cData value from the page\n    'pagedata' =&gt; $chlPageData,   \/\/ the chlPageData value\n    'action' =&gt; 'managed',        \/\/ optional, when the page declares one\n]);\n\necho $result['code'];\necho $result['userAgent'];        \/\/ needed for the submit<\/pre>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The user agent is mandatory here<\/h2>\n<p>Turnstile binds 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 <code>$result['userAgent']<\/code>. Submit with cURL&#8217;s default user agent instead and Cloudflare rejects a token that is otherwise perfectly good.<\/p>\n<p><code>userAgent<\/code> is populated for Turnstile only. It is absent for every other CAPTCHA type, which is why this catches people reusing a working reCAPTCHA helper.<\/p>\n<pre data-enlighter-language=\"php\" class=\"EnlighterJSRAW\">$ch = curl_init($pageUrl);\n\ncurl_setopt_array($ch, [\n    CURLOPT_POST =&gt; true,\n    CURLOPT_RETURNTRANSFER =&gt; true,\n    \/\/ Send back the exact user agent the solve was performed with.\n    CURLOPT_USERAGENT =&gt; $result['userAgent'],\n    CURLOPT_POSTFIELDS =&gt; http_build_query([\n        'cf-turnstile-response' =&gt; $result['code'],\n    ]),\n]);\n\n$response = curl_exec($ch);\ncurl_close($ch);<\/pre>\n<p>If a token is being rejected and the cData is fresh, this is nearly always the cause.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Proxies<\/h2>\n<pre data-enlighter-language=\"php\" class=\"EnlighterJSRAW\">$result = $solver-&gt;turnstile($sitekey, $pageUrl, [\n    'data' =&gt; $cData,\n    'pagedata' =&gt; $chlPageData,\n    'proxy' =&gt; ['type' =&gt; 'HTTPS', 'uri' =&gt; 'user:pass@1.2.3.4:3128'],\n]);<\/pre>\n<p>Solve through the same egress you will submit from when the challenge is geo-sensitive. Proxies work for Turnstile, reCAPTCHA and GeeTest, but not for image CAPTCHAs, which never touch the target site.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Errors<\/h2>\n<pre data-enlighter-language=\"php\" class=\"EnlighterJSRAW\">use CapSkip\\Exceptions\\ApiException;\nuse CapSkip\\Exceptions\\NetworkException;\nuse CapSkip\\Exceptions\\TimeoutException;\n\ntry {\n    $result = $solver-&gt;turnstile($sitekey, $pageUrl, $options);\n} catch (NetworkException $e) {\n    \/\/ CapSkip is not running on the configured port\n} catch (ApiException $e) {\n    \/\/ often a stale cData, since it is bound to one page load\n} catch (TimeoutException $e) {\n    \/\/ exceeded recaptchaTimeout\n}<\/pre>\n<p>Everything lives under <code>CapSkip\\Exceptions<\/code>, unlike the Python and Node SDKs which export exceptions from the package root.<\/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;\">Do widgets ever need cData?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No, and passing empty values makes the solve fail rather than helping. Only full-page interstitial challenges use them.<\/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;\">My token is valid but gets rejected.<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Almost always the user agent. Set <code>CURLOPT_USERAGENT<\/code> to <code>$result['userAgent']<\/code> rather than leaving cURL&#8217;s default. The next most likely cause is a stale cData, which only survives one page load.<\/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;\">Should this run inside a web request?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Preferably not. PHP is synchronous and a Turnstile solve takes a few seconds, so doing it during a page render blocks a worker. Move it into a queued job or a CLI worker.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Summary<\/h2>\n<p>Widgets take a sitekey and a page URL. Challenge pages need <code>data<\/code> and <code>pagedata<\/code> read fresh from the page, and the token has to be submitted with <code>$result['userAgent']<\/code>. Import exceptions from <code>CapSkip\\Exceptions<\/code>, and keep solves off your request threads.<\/p>\n<p>Other languages are on the <a href=\"https:\/\/capskip.com\/cloudflare-turnstile-solver\/\">Cloudflare Turnstile solver<\/a> page, parameter details in the <a href=\"https:\/\/capskip.com\/api-docs\/\">API documentation<\/a>, and the wider PHP surface on the <a href=\"https:\/\/capskip.com\/php-captcha-solver\/\">PHP CAPTCHA solver<\/a> page. CapSkip is an <a href=\"https:\/\/capskip.com\/\">unlimited captcha solver<\/a> running on your own machine.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Turnstile \u5c0f\u7ec4\u4ef6\u662f\u4e00\u4e2a\u53cc\u53c2\u6570\u7684 PHP \u8c03\u7528\u3002\u6311\u6218\u9875\u9762\u9700\u8981\u53e6\u5916\u4e24\u4e2a\u503c\u4ee5\u53ca\u968f token \u8fd4\u56de\u7684 user agent\uff0c\u5426\u5219\u4f1a\u88ab\u62d2\u7edd\u3002<\/p>","protected":false},"author":1,"featured_media":24984,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Solve Cloudflare Turnstile in PHP | CapSkip","rank_math_description":"Turnstile widgets need a sitekey. Challenge pages also need cData, chlPageData and the returned user agent. Here is the PHP code for both cases.","rank_math_focus_keyword":"solve cloudflare turnstile in php","footnotes":""},"categories":[70],"tags":[],"class_list":["post-24985","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\/24985","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=24985"}],"version-history":[{"count":1,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/24985\/revisions"}],"predecessor-version":[{"id":24998,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/24985\/revisions\/24998"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/24984"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=24985"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=24985"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=24985"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}