Migration Guide
Migrate from reCAPTCHA, Turnstile, or hCaptcha to Crovly.
From reCAPTCHA v2
Frontend
Before (reCAPTCHA v2):
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<form action="/submit" method="POST">
<div class="g-recaptcha" data-sitekey="RECAPTCHA_SITE_KEY"></div>
<button type="submit">Submit</button>
</form>After (Crovly):
<script src="https://get.crovly.com/widget.js"
data-site-key="YOUR_CROVLY_SITE_KEY"></script>
<form action="/submit" method="POST">
<div id="crovly-captcha"></div>
<button type="submit">Submit</button>
</form>Backend
Before (reCAPTCHA v2):
const res = await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `secret=${RECAPTCHA_SECRET}&response=${req.body['g-recaptcha-response']}`,
});
const { success } = await res.json();After (Crovly):
const res = await fetch('https://api.crovly.com/verify-token', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
token: req.body['crovly-token'],
expectedIp: req.ip,
}),
});
const { success, score } = await res.json();From reCAPTCHA v3
Frontend
Before (reCAPTCHA v3):
<script src="https://www.google.com/recaptcha/api.js?render=SITE_KEY"></script>
<script>
grecaptcha.ready(() => {
grecaptcha.execute('SITE_KEY', { action: 'submit' }).then((token) => {
document.getElementById('recaptcha-token').value = token;
});
});
</script>
<form action="/submit" method="POST">
<input type="hidden" id="recaptcha-token" name="g-recaptcha-response" />
<button type="submit">Submit</button>
</form>After (Crovly):
<script src="https://get.crovly.com/widget.js"
data-site-key="YOUR_CROVLY_SITE_KEY"
data-size="invisible"></script>
<form action="/submit" method="POST">
<div id="crovly-captcha"></div>
<button type="submit">Submit</button>
</form>Crovly's invisible mode replaces reCAPTCHA v3's background scoring. The hidden input is injected automatically — no manual JavaScript is required.
Backend
The backend change is the same as reCAPTCHA v2 above. Replace the Google siteverify call with the Crovly /verify-token call.
Key difference: reCAPTCHA v3 returns a score that you threshold yourself. Crovly returns both a score and a passed boolean based on the threshold you set in the dashboard, so you can use either approach.
From Cloudflare Turnstile
Frontend
Before (Turnstile):
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form action="/submit" method="POST">
<div class="cf-turnstile" data-sitekey="TURNSTILE_SITE_KEY"></div>
<button type="submit">Submit</button>
</form>After (Crovly):
<script src="https://get.crovly.com/widget.js"
data-site-key="YOUR_CROVLY_SITE_KEY"></script>
<form action="/submit" method="POST">
<div id="crovly-captcha"></div>
<button type="submit">Submit</button>
</form>Backend
Before (Turnstile):
const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: TURNSTILE_SECRET,
response: req.body['cf-turnstile-response'],
}),
});
const { success } = await res.json();After (Crovly):
const res = await fetch('https://api.crovly.com/verify-token', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
token: req.body['crovly-token'],
expectedIp: req.ip,
}),
});
const { success, score } = await res.json();Key Differences
| Feature | reCAPTCHA | Turnstile | hCaptcha | Crovly |
|---|---|---|---|---|
| Hidden input name | g-recaptcha-response | cf-turnstile-response | h-captcha-response | crovly-token |
| Verify URL | google.com/.../siteverify | cloudflare.com/.../siteverify | hcaptcha.com/siteverify | api.crovly.com/verify-token |
| Auth method | secret in body | secret in body | secret in body | Authorization: Bearer header |
| Request format | URL-encoded | JSON | URL-encoded | JSON |
| IP binding | Optional | No | No | Optional (expectedIp) |
| Score returned | v3 only | No | Enterprise only | Always (0.0-1.0) |
| Self-hostable | No | No | No | Contact us |
| Image puzzles | Yes | No | Yes | No (PoW only) |
From hCaptcha
Frontend
Before (hCaptcha):
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<form action="/submit" method="POST">
<div class="h-captcha" data-sitekey="HCAPTCHA_SITE_KEY"></div>
<button type="submit">Submit</button>
</form>After (Crovly):
<script src="https://get.crovly.com/widget.js"
data-site-key="YOUR_CROVLY_SITE_KEY"></script>
<form action="/submit" method="POST">
<div id="crovly-captcha"></div>
<button type="submit">Submit</button>
</form>Backend
Before (hCaptcha):
const res = await fetch('https://api.hcaptcha.com/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `secret=${HCAPTCHA_SECRET}&response=${req.body['h-captcha-response']}`,
});
const { success } = await res.json();After (Crovly):
const res = await fetch('https://api.crovly.com/verify-token', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
token: req.body['crovly-token'],
expectedIp: req.ip,
}),
});
const { success, score } = await res.json();Migration Checklist
- Create a Crovly account at app.crovly.com and add your site.
- Copy your site key (public) and API key (private) from the dashboard.
- Replace the frontend script tag and container div as shown above.
- Update your backend to call
api.crovly.com/verify-tokenwith theAuthorizationheader. - Update the form field name in your backend from
g-recaptcha-responseorcf-turnstile-responsetocrovly-token. - Test with test keys (
test_always_pass/test_always_fail) before going live. - Remove the old captcha script tags and API keys.
- Update your CSP if applicable — add
get.crovly.comtoscript-src,api.crovly.comtoconnect-src, andblob:toworker-src.