Most reCAPTCHA migrations fail for boring reasons, not cryptography. A hidden field gets renamed, a server route still trusts the browser, or one old form keeps calling Google after the rest of the site has moved on. This guide is the practical migration sequence we would use ourselves to replace reCAPTCHA with Playtcha without breaking signup, contact, or checkout flows.
Why teams are migrating now
The usual trigger is not just "we want a different CAPTCHA." It is one of four specific problems:
- Privacy review pressure. reCAPTCHA is hard to defend when legal or procurement starts asking what data is sent to Google and why.
- UX fatigue. Invisible scoring looks elegant until a real user trips a low score and gets an image grid at the worst possible moment.
- Performance budget. Auth and lead forms are exactly where most teams do not want another large third-party payload. We covered the bundle cost in why your CAPTCHA shouldn't be 250 KB.
- Trust. Many teams no longer want anti-bot protection that depends on behavioral scoring they cannot inspect or explain.
The honest part: Playtcha is not a drop-in invisible clone. It is a visible, short minigame. If your only goal is the lowest possible user interaction, read our reCAPTCHA alternatives comparisonfirst and decide whether explicit interaction is acceptable for your flow.
Inventory the current reCAPTCHA touchpoints
Before you swap any script tag, list every place where reCAPTCHA is involved. Most teams remember the signup form and forget the contact form, password reset, invite flow, or background score checks on JSON endpoints.
At minimum, search for these patterns in your app:
grecaptchag-recaptchag-recaptcha-response- Your current secret key env vars and any server route that posts to Google verify endpoints
Write down three things for each protected flow: where the widget is mounted, what hidden field or callback it produces, and where the server verifies it. If you do not have this map, your migration will be half-finished for weeks.
Swap the client-side widget
The client-side step should stay boring. Replace the reCAPTCHA script and widget markup with Playtcha's hosted widget, then keep the rest of the form unchanged.
<!-- Load Playtcha -->
<script src="https://playtcha.com/v1/widget.js" async defer></script>
<!-- Mount it inside your form -->
<form action="/signup" method="POST">
<input name="email" type="email" required />
<div class="playtcha" data-sitekey="YOUR_PUBLIC_SITE_KEY"></div>
<button type="submit">Create account</button>
</form>On success, the widget writes a hidden input named playtcha-token into the form. That is the value your server reads on submit. Compared to reCAPTCHA, the migration usually means:
- Remove the Google loader script and any direct calls to
grecaptcha.renderorgrecaptcha.execute. - Remove code that waits for a client callback to decide whether the form may submit.
- Make sure your CSP allows loading the widget from
https://playtcha.com.
If your current flow depends on reCAPTCHA v3 action names and score thresholds, treat that logic as legacy behavior to remove, not behavior to recreate. Playtcha is a pass or fail gate verified on your server.
Move verification to the server
This is the real migration step. Browser-side CAPTCHA checks are only a UX layer. The actual security contract lives on the server. Your submit handler reads playtcha-token, posts it tohttps://playtcha.com/v1/verify with your secret key, and rejects the request unless the response says success.
const playtchaToken = String(req.body["playtcha-token"] ?? "");
if (!playtchaToken) {
return res.status(400).json({ error: "captcha_missing" });
}
const verifyRes = await fetch("https://playtcha.com/v1/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
secret: process.env.PLAYTCHA_SECRET,
token: playtchaToken,
}),
signal: AbortSignal.timeout(3000),
});
const result = await verifyRes.json();
if (!verifyRes.ok || result.success !== true) {
return res.status(400).json({ error: "captcha_failed" });
}
// Only now process the signup / form / checkout.Three rules matter here:
- Verify before expensive work. Do not create the user, send the email, or hit your database before the human gate passes.
- Never send your secret to the browser. Keep it in server-only env vars.
- Do not call verify from frontend code. That is intentionally blocked by CORS and would leak the wrong trust model even if it were allowed.
If you want a concrete framework example, see adding a CAPTCHA to a Supabase signup flow. The same server-side verify shape applies outside Supabase too.
Roll out without breaking forms
The safest order is not "replace reCAPTCHA everywhere in one deploy." Use a staged rollout:
- Start with the highest-abuse form. Usually signup, contact, or waitlist.
- Wire the new site key and secret in production first.Confirm the domain allowlist is correct before you flip traffic.
- Replace the client widget and server verify together.Do not ship one without the other.
- Watch failures for one day. Look for missing tokens, rejected origins, and forms still posting the old field name.
- Then migrate the long tail. Password reset, invite, feedback, and any hidden admin tools that still expose public forms.
A short overlap period is fine if you need one, but do not leave two CAPTCHA systems permanently wired into the same form. It increases complexity, creates analytics noise, and makes incident response harder.
Common migration mistakes
Forgetting the old hidden field name
reCAPTCHA integrations often read g-recaptcha-response. Playtcha submits playtcha-token. If your backend still reads the old name, every request will look like a CAPTCHA failure.
Keeping score-based branching alive
reCAPTCHA v3 encourages threshold logic like "0.5 means let them through, 0.2 means challenge, 0.1 means block." Playtcha is not a score feed. Remove that policy tree and replace it with an explicit verify gate.
Skipping domain configuration
Make sure the project domain list matches your real hosts before launch, including staging if you intend to test there. Domain mismatches are one of the easiest ways to create self-inflicted launch failures.
Failing open by accident
Decide your failure mode deliberately. If verify is unavailable, do you block the request, rate-limit harder, or queue it for review? Do not let a catch block silently continue as though the CAPTCHA passed.
Marketing the migration as invisible
The product change is user-visible. That is part of the point. Teams who expect a silent scoring swap should evaluate whether they actually want a visible human gate before migrating.
FAQ
Can I migrate one form at a time?
Yes. In fact, that is the safer path. Start with the form that gets the most abuse and expand once the verify flow is stable.
Do I need to keep reCAPTCHA as a fallback?
Usually no. During a short rollout window, maybe. Long term, no. Two CAPTCHA systems in one app usually means twice the failure surface and no one fully understands the final control flow.
What if I only used reCAPTCHA v3 scores on the backend?
Then your migration is mostly a server policy migration. Remove the score threshold logic, add the widget where the form is submitted, and gate the request on the verify response instead.
What if I need lower-friction UX than a game?
Then you may prefer a different product. That is a legitimate outcome. We would rather say that plainly than pretend every form should use the same anti-bot model.
Where should I go next?
Start with the quickstart docs if you are ready to wire it in, or compare the tradeoffs in reCAPTCHA alternatives in 2026if you are still evaluating vendors.