Migrate from reCAPTCHA in 10 minutes

Updated

This guide replaces Google reCAPTCHA v2 with Bot403 on a form. You change three items: the script tag, the widget element and the server call. The form fields, the form action and your form handling do not change.

The example is an Express 5 form on Node.js. The new code was run on 23 October 2026 against a local copy of the verify API. On the same day, the format of the old siteverify answer was examined with the public test key from Google.

1. Create a site (2 minutes)

Sign in with your email. Then create a site. Copy its public key (pk_) and its secret key (sk_). If you list allowed origins, add the domains of the pages with the form. For tests with this list, also add localhost.

2. The HTML (2 minutes)

Replace the script tag. Then delete the widget element. The Bot403 script finds the form automatically. Before the browser sends the form, the script adds a hidden _bot403 field. There is no box to put on the page and nothing to click.

contact.html
 <head>
-  <script src="https://www.google.com/recaptcha/api.js" async defer></script>
+  <script src="https://bot403.com/v1/bot403.js" data-bot403-key="pk_live_YOUR_PUBLIC_KEY" async></script>
 </head>
 <body>
   <form action="/contact" method="POST">
     <label>Email <input name="email" type="email" required></label>
     <label>Message <textarea name="message" required></textarea></label>
-    <div class="g-recaptcha" data-sitekey="YOUR_RECAPTCHA_SITE_KEY"></div>
     <button type="submit">Send</button>
   </form>

3. The server (4 minutes)

Replace the call to siteverify with a call to https://bot403.com/api/v1/verify. Send the full form, with the secret key as a Bearer token. Bot403 reads its own field and the honeypot field. In the answer, ok replaces success from reCAPTCHA, and reason replaces error-codes.

server.mjs
 app.post("/contact", async (req, res) => {
-  const r = await fetch("https://www.google.com/recaptcha/api/siteverify", {
-    method: "POST",
-    body: new URLSearchParams({
-      secret: process.env.RECAPTCHA_SECRET_KEY,
-      response: req.body["g-recaptcha-response"],
-    }),
-  });
-  const result = await r.json();
-  if (!result.success) return res.status(400).send("Try again");
+  let result = { ok: true };
+  try {
+    const r = await fetch("https://bot403.com/api/v1/verify", {
+      method: "POST",
+      headers: { Authorization: `Bearer ${process.env.BOT403_SECRET_KEY}` },
+      body: new URLSearchParams(req.body),
+      signal: AbortSignal.timeout(5000),
+    });
+    result = await r.json();
+  } catch (err) {
+    console.error("bot403 verify failed, letting the form through", err);
+  }
+  if (!result.ok && result.reason !== "quota") return res.status(400).send("Try again");
   res.send("Thanks.");
 });
.env
-RECAPTCHA_SECRET_KEY=6Lc...
+BOT403_SECRET_KEY=sk_live_YOUR_SECRET_KEY

Two decisions in this code are different from a typical reCAPTCHA handler. If Bot403 does not answer in 5 seconds, this code accepts the form. If the reason is quota, the code also accepts the form. This reason comes only in a capped month, after two calendar months over the plan in a row. If you want to refuse the form in these two cases, change both lines. The troubleshooting page explains every reason.

4. Deploy and test (2 minutes)

  1. Deploy the HTML and the server change together.
  2. Send a message with the form. The site in your dashboard then shows one check.
  3. If your pages are cached, a visitor can still get an old page for some time. Then the visitor sends the form on that page with a reCAPTCHA token. For this case, you can keep the old siteverify call for one day. Use it only for forms that have g-recaptcha-response and no _bot403 field. After that day, delete the old call. Then delete the reCAPTCHA keys in the Google Cloud console.

Clean up

If your site sends a Content Security Policy, remove the Google entries. Then allow Bot403:

Content-Security-Policy
-script-src 'self' https://www.google.com https://www.gstatic.com;
-frame-src https://www.google.com;
+script-src 'self' https://bot403.com;
+connect-src 'self' https://bot403.com;
+worker-src blob:;

If the policy does not have worker-src blob:, the script still works. But the script then solves the puzzle on the main thread. As a result, the page can stop responding for a fraction of a second.

Update your privacy policy and your cookie banner. reCAPTCHA sets a cookie and sends visitor data to Google. The Bot403 script sets no cookie and reads no storage. It sends one request to Bot403, which stores no visitor IP address. The privacy page and the data processing agreement give the details for your own policy. The guide GDPR and CAPTCHAs tells why this is important in the EU.

Notes for other setups

  • reCAPTCHA v3. Delete the grecaptcha.ready() and grecaptcha.execute() calls. Also delete the hidden field that these calls fill. On the server, delete the score threshold. Bot403 has no score: a token is good, or it fails with a reason.
  • Forms sent with fetch. Get a token from the script at the same place where you got one from reCAPTCHA. Each token passes the check one time only. Thus, get a new token for each request.
form.js
-const token = grecaptcha.getResponse();
-body.set("g-recaptcha-response", token);
+body.set("_bot403", await window.Bot403.solve());
  • PHP. Replace the siteverify request. Send $_POST to the verify API in a POST request, with the Bearer header. The verify page has this code, ready to copy.
  • Laravel, Django, Next.js, Astro, Hugo. Each framework has a guide with the full form and the server code: Laravel, Django, Next.js, Astro, Hugo.
  • reCAPTCHA Enterprise. The createAssessment call does the job of siteverify. Replace it in the same way. Then delete the risk score logic.
  • Go, Ruby, Bun, Deno, Workers. The verify page has the server call for each of these.

For a comparison of the two on price, privacy and setup, read Bot403 as a reCAPTCHA alternative.