{"id":24983,"date":"2026-08-06T10:17:51","date_gmt":"2026-08-06T10:17:51","guid":{"rendered":"https:\/\/capskip.com\/?p=24983"},"modified":"2026-08-06T10:17:51","modified_gmt":"2026-08-06T10:17:51","slug":"recaptcha-v3-php","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/recaptcha-v3-php\/","title":{"rendered":"\u5982\u4f55\u7528 PHP \u8bc6\u522b reCAPTCHA v3 \u5e76\u8bbe\u7f6e action"},"content":{"rendered":"<p>reCAPTCHA v3 never renders a challenge. It scores the visit quietly and gives the page a token, which the site&#8217;s backend then verifies. From your code that means there is nothing to click, so the whole task is producing a token the site accepts. In PHP that is the same <code>recaptcha<\/code> method you would use for v2, with a version flag in the options array.<\/p>\n<p>The thing that decides whether it works is the action.<\/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, shared with Turnstile and GeeTest\n]);<\/pre>\n<p>CapSkip runs on your own machine, so the desktop app has to be open first.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The basic call<\/h2>\n<pre data-enlighter-language=\"php\" class=\"EnlighterJSRAW\">$result = $solver-&gt;recaptcha(\n    '6Lc...YOUR_SITEKEY',\n    'https:\/\/example.com\/checkout',\n    [\n        'version' =&gt; 'v3',\n        'action' =&gt; 'submit',\n    ]\n);\n\necho $result['code'];   \/\/ the v3 token<\/pre>\n<p>Two things differ from v2. <code>version<\/code> must be <code>v3<\/code>, and <code>action<\/code> should match whatever the page passes to <code>grecaptcha.execute<\/code>. Omit it and it defaults to <code>verify<\/code>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Why the action matters<\/h2>\n<p>Actions are labels a site attaches to each protected interaction so a login and a checkout can be scored separately. Most backends check that the action carried by the token matches the one they expected for that endpoint.<\/p>\n<p>Send the wrong label and the token is genuine but tagged for a different interaction, which many verifiers reject outright. Read the real value from the page instead of guessing:<\/p>\n<pre data-enlighter-language=\"php\" class=\"EnlighterJSRAW\">$html = file_get_contents('https:\/\/example.com\/checkout');\n\n\/\/ Sites normally call execute() with the action as a string literal.\npreg_match('\/execute\\([^,]+,\\s*\\{\\s*action:\\s*[\\'&quot;]([^\\'&quot;]+)\/', $html, $m);\n$action = $m[1] ?? 'verify';\n\n$result = $solver-&gt;recaptcha($sitekey, $pageUrl, [\n    'version' =&gt; 'v3',\n    'action' =&gt; $action,\n]);<\/pre>\n<p>Common values are <code>login<\/code>, <code>submit<\/code>, <code>homepage<\/code> and <code>checkout<\/code>, but they are arbitrary strings picked by whoever built the site.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Enterprise v3<\/h2>\n<pre data-enlighter-language=\"php\" class=\"EnlighterJSRAW\">$result = $solver-&gt;recaptcha($sitekey, $pageUrl, [\n    'version' =&gt; 'v3',\n    'enterprise' =&gt; 1,\n    'action' =&gt; 'submit',\n]);<\/pre>\n<p>Enterprise is an orthogonal flag rather than a separate product, so it stacks on the v3 call. Tell them apart by the script the page loads: Enterprise pulls <code>enterprise.js<\/code>, standard pulls <code>api.js<\/code>. Guessing wrong fails the solve rather than returning a bad token, so it costs nothing to test.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Submitting the token<\/h2>\n<pre data-enlighter-language=\"php\" class=\"EnlighterJSRAW\">$ch = curl_init('https:\/\/example.com\/checkout');\n\ncurl_setopt_array($ch, [\n    CURLOPT_POST =&gt; true,\n    CURLOPT_RETURNTRANSFER =&gt; true,\n    CURLOPT_HTTPHEADER =&gt; ['Content-Type: application\/json'],\n    CURLOPT_POSTFIELDS =&gt; json_encode([\n        'token' =&gt; $result['code'],\n        'order_id' =&gt; '...',\n    ]),\n]);\n\n$response = curl_exec($ch);\ncurl_close($ch);<\/pre>\n<p>Unlike v2 there is no standard form widget constraining the shape, so v3 integrations vary. Some use a hidden <code>g-recaptcha-response<\/code> input, others post JSON with a custom key. Check the page&#8217;s own JavaScript before assuming a field name.<\/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;recaptcha($sitekey, $pageUrl, ['version' =&gt; 'v3']);\n} catch (NetworkException $e) {\n    \/\/ CapSkip is not running on the configured port\n} catch (ApiException $e) {\n    \/\/ the sitekey or pageurl was rejected\n} catch (TimeoutException $e) {\n    \/\/ exceeded recaptchaTimeout\n}<\/pre>\n<p>Note the <code>CapSkip\\Exceptions<\/code> namespace. Copying imports from the Python or Node examples is a common source of fatal errors here.<\/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 check the score before submitting?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No. The score lives with Google and is only revealed to the site owner when their backend verifies the token. From the client side you receive a token and nothing else, so there is nothing to inspect or filter on beforehand.<\/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 I solve inside a web request?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Preferably not. PHP is synchronous and a solve can take several seconds, so doing it during a page render ties up a worker for the duration. Move it into a queued job or a CLI worker.<\/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 if the page never sets an action?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Some pages call <code>execute<\/code> without one, in which case the default <code>verify<\/code> is correct. If your regex finds nothing, that is usually the reason rather than a parsing bug.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Summary<\/h2>\n<p>Set <code>version<\/code> to <code>v3<\/code>, match <code>action<\/code> to what the page executes, add <code>enterprise<\/code> when it loads <code>enterprise.js<\/code>, import exceptions from <code>CapSkip\\Exceptions<\/code>, and submit the token quickly because it expires in about two minutes.<\/p>\n<p>Other languages are covered on the <a href=\"https:\/\/capskip.com\/recaptcha-v3-solver\/\">reCAPTCHA v3 solver<\/a> page, Enterprise details on the <a href=\"https:\/\/capskip.com\/recaptcha-enterprise-solver\/\">Enterprise solver<\/a> page, and the wider PHP surface on the <a href=\"https:\/\/capskip.com\/php-captcha-solver\/\">PHP CAPTCHA solver<\/a> page. See a token generated live on our <a href=\"https:\/\/capskip.com\/captcha-demo\/recaptcha-v3\/\">v3 demo<\/a>. CapSkip is a <a href=\"https:\/\/capskip.com\/\">captcha solver<\/a> that runs on your own hardware.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>v3 \u4e0e v2 \u662f\u540c\u4e00\u4e2a PHP \u65b9\u6cd5\uff0c\u53ea\u591a\u4e86\u4e00\u4e2a version \u6807\u5fd7\u3002\u51b3\u5b9a\u5b83\u662f\u5426\u594f\u6548\u7684\u7ec6\u8282\u662f action\uff0c\u5b83\u5fc5\u987b\u4e0e\u9875\u9762\u6267\u884c\u7684\u5185\u5bb9\u4e00\u81f4\u3002<\/p>","protected":false},"author":1,"featured_media":24982,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Solve reCAPTCHA v3 in PHP | CapSkip","rank_math_description":"reCAPTCHA v3 in PHP needs a version flag and an action that matches the page. Here is the call for standard and Enterprise, plus how to find the action.","rank_math_focus_keyword":"solve recaptcha v3 in php","footnotes":""},"categories":[71],"tags":[],"class_list":["post-24983","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-google-recaptcha"],"_links":{"self":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/24983","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=24983"}],"version-history":[{"count":1,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/24983\/revisions"}],"predecessor-version":[{"id":24997,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/24983\/revisions\/24997"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/24982"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=24983"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=24983"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=24983"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}