Crovly

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

FeaturereCAPTCHATurnstilehCaptchaCrovly
Hidden input nameg-recaptcha-responsecf-turnstile-responseh-captcha-responsecrovly-token
Verify URLgoogle.com/.../siteverifycloudflare.com/.../siteverifyhcaptcha.com/siteverifyapi.crovly.com/verify-token
Auth methodsecret in bodysecret in bodysecret in bodyAuthorization: Bearer header
Request formatURL-encodedJSONURL-encodedJSON
IP bindingOptionalNoNoOptional (expectedIp)
Score returnedv3 onlyNoEnterprise onlyAlways (0.0-1.0)
Self-hostableNoNoNoContact us
Image puzzlesYesNoYesNo (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

  1. Create a Crovly account at app.crovly.com and add your site.
  2. Copy your site key (public) and API key (private) from the dashboard.
  3. Replace the frontend script tag and container div as shown above.
  4. Update your backend to call api.crovly.com/verify-token with the Authorization header.
  5. Update the form field name in your backend from g-recaptcha-response or cf-turnstile-response to crovly-token.
  6. Test with test keys (test_always_pass / test_always_fail) before going live.
  7. Remove the old captcha script tags and API keys.
  8. Update your CSP if applicable — add get.crovly.com to script-src, api.crovly.com to connect-src, and blob: to worker-src.

On this page