Challenge migration guide

# Migrate from reCAPTCHA to Botpoison Challenge

Three edits: swap the script, move the key onto the form, and replace the siteverify call with one verify call. Your form keeps its fields, its layout and its thank-you page. The same edits work for the v2 checkbox, the invisible badge and v3.

Last verified 16 October 2026

## Why sites move off reCAPTCHA

Google has moved every reCAPTCHA key into a Google Cloud project, finishing in early 2026. The first 10,000 assessments a month stay free. Past that, a key without billing set up stops returning scores until the next month.

In the EU, the French data protection authority and an Austrian court have both dealt with reCAPTCHA and consent. [reCAPTCHA and the GDPR](https://botpoison.com/learn/recaptcha-gdpr/) sets out what each one decided.

And the v2 checkbox still asks some visitors to pick out images, the one task many people with low vision or a screen reader cannot do.

## What maps to what

|  | reCAPTCHA | Challenge |
| --- | --- | --- |
| Script | `google.com/recaptcha/api.js` | `unpkg.com/@botpoison/browser` |
| On the form | A `g-recaptcha` div with `data-sitekey` | `data-botpoison-public-key` on the form itself |
| Field posted | `g-recaptcha-response` | `_botpoison` |
| Server check | POST to `/recaptcha/api/siteverify` with the secret and the response | The same form POSTed to `/verify`, with `secretKey` and `solution` |
| Result | `success`, and on v3 a `score` from 0.0 to 1.0 | `ok`, true or false |

## The three edits. Off reCAPTCHA, onto Challenge

### Swap the script

Remove Google's script tag and add ours. It loads async, like theirs did.

HTML

```diff
-<script src="https://www.google.com/recaptcha/api.js" async defer></script>
+<script src="https://unpkg.com/@botpoison/browser" async></script>
```

### Swap the key on the form

Delete the widget's div and put your public key on the form itself. Nothing renders in its place.

HTML

```diff
 <form
   action="/contact"
   method="POST"
+  data-botpoison-public-key="pk_xxxxxxxx"
 >
   <textarea name="message"></textarea>
-  <div class="g-recaptcha" data-sitekey="your_site_key"></div>
   <button type="submit">Send</button>
 </form>
```

### Swap the check on your server

Post the same form to /verify: secretKey and _botpoison in place of secret and g-recaptcha-response, and ok in place of success.

Node.js:

JavaScript

```diff
-const google = await fetch("https://www.google.com/recaptcha/api/siteverify", {
+const verified = await fetch("https://api.botpoison.com/verify", {
   method: "POST",
   body: new URLSearchParams({
-    secret: process.env.RECAPTCHA_SECRET,
-    response: req.body["g-recaptcha-response"],
+    secretKey: process.env.BOTPOISON_CHALLENGE_SECRET_KEY,
+    solution: req.body._botpoison,
   }),
 });
-const { success: ok } = await google.json();
+const { ok } = await verified.json();
 if (!ok) return res.status(403).end();
```

PHP:

PHP

```diff
-$google = file_get_contents("https://www.google.com/recaptcha/api/siteverify", false, stream_context_create(["http" => [
+$verified = file_get_contents("https://api.botpoison.com/verify", false, stream_context_create(["http" => [
   "method" => "POST",
   "header" => "Content-Type: application/x-www-form-urlencoded",
   "content" => http_build_query([
-    "secret" => getenv("RECAPTCHA_SECRET"),
-    "response" => $_POST["g-recaptcha-response"],
+    "secretKey" => getenv("BOTPOISON_CHALLENGE_SECRET_KEY"),
+    "solution" => $_POST["_botpoison"],
   ]),
 ]]));
-$ok = json_decode($google)->success;
+$ok = json_decode($verified)->ok;
 if (!$ok) { http_response_code(403); exit; }
```

Python:

Python

```diff
-google = requests.post("https://www.google.com/recaptcha/api/siteverify", data={
-    "secret": os.environ["RECAPTCHA_SECRET"],
-    "response": request.form["g-recaptcha-response"],
+verified = requests.post("https://api.botpoison.com/verify", data={
+    "secretKey": os.environ["BOTPOISON_CHALLENGE_SECRET_KEY"],
+    "solution": request.form["_botpoison"],
 })
-ok = google.json()["success"]
+ok = verified.json()["ok"]
 if not ok:
     abort(403)
```

Go:

Go

```diff
-res, err := http.PostForm("https://www.google.com/recaptcha/api/siteverify", url.Values{
-  "secret":   {os.Getenv("RECAPTCHA_SECRET")},
-  "response": {r.FormValue("g-recaptcha-response")},
+res, err := http.PostForm("https://api.botpoison.com/verify", url.Values{
+  "secretKey": {os.Getenv("BOTPOISON_CHALLENGE_SECRET_KEY")},
+  "solution":  {r.FormValue("_botpoison")},
 })
 if err != nil {
   http.Error(w, "", http.StatusBadGateway)
   return
 }
-var out struct{ Ok bool `json:"success"` }
+var out struct{ Ok bool `json:"ok"` }
 json.NewDecoder(res.Body).Decode(&out)
 if !out.Ok {
   http.Error(w, "", http.StatusForbidden)
   return
 }
```

HTTP:

Shell

```diff
-curl "https://www.google.com/recaptcha/api/siteverify" \
-  --data-urlencode "secret=$RECAPTCHA_SECRET" \
-  --data-urlencode "response=<the g-recaptcha-response field>"
-# {"success": true, ...}
+curl "https://api.botpoison.com/verify" \
+  --data-urlencode "secretKey=$BOTPOISON_CHALLENGE_SECRET_KEY" \
+  --data-urlencode "solution=<the _botpoison field>"
+# {"ok": true, ...}
```

## What behaves differently

No widget. Nothing renders on the page, so remove any CSS or layout space kept for the checkbox or the badge.

Nothing runs on page load. The check starts when the visitor presses Send, so a visitor who never submits costs nothing.

No score to tune on v3. Challenge answers pass or fail. If you used the v3 score to catch spam that people type, pair Challenge with [Classify](https://botpoison.com/classify/), which scores the message itself.

## Questions

**Do I still need a Google Cloud project?**

No. Once the three edits ship, nothing on the form calls Google.

**I use the invisible badge. Is this different?**

The same three edits apply. The button that carried the `g-recaptcha` class goes back to a plain submit button.

**Does it work with my framework?**

Yes. The [Challenge getting started guide](https://botpoison.com/documentation/challenge/getting-started/) has a tab each for plain HTML, JavaScript, React, Vue, Angular and Svelte.

## Sources

- [Migrate reCAPTCHA keys to Google Cloud](https://docs.cloud.google.com/recaptcha/docs/migration-overview) Google Cloud documentation
- [Verifying the user's response](https://developers.google.com/recaptcha/docs/verify) Google reCAPTCHA documentation
- [Google to migrate all reCAPTCHA services to Cloud Platform](https://www.searchenginejournal.com/google-to-migrate-all-recaptcha-services-to-cloud-platform/538749/) Search Engine Journal, January 2025

reCAPTCHA is a trademark of Google. Botpoison is not affiliated with Google.

## Other migration guides

### To Challenge

Invisible CAPTCHA alternative

- [From Turnstile](https://botpoison.com/migrate/from-turnstile/)
  Drop the cf-turnstile widget. The check runs fresh on each Send and moves to one verify call.
- [From hCaptcha](https://botpoison.com/migrate/from-hcaptcha/)
  Remove the h-captcha div and the form-encoded siteverify request. Nobody labels images again.

### To Classify

Spam classifier

- [From Akismet](https://botpoison.com/migrate/from-akismet/)
  comment-check answers "true" or "false" as text. Classify answers a label, and a score to hold the borderline ones on.
- [From OOPSpam](https://botpoison.com/migrate/from-oopspam/)
  A score from 0 to 6 against your threshold becomes a label, with the score kept for a review queue.
