Switch from reCAPTCHA

Migrate from Google reCAPTCHA to gkCAPTCHA in under 15 minutes.

Why Switch?

  • PDPL compliance: gkCAPTCHA is fully compliant with Saudi Arabia's Personal Data Protection Law. Google reCAPTCHA transfers user data to US servers, which may conflict with PDPL data localization requirements.
  • Data residency: All data is processed and stored in our Riyadh data center. No user data ever leaves Saudi Arabia.
  • Privacy-first: gkCAPTCHA uses no tracking cookies, no persistent fingerprinting, and no cross-site tracking. Behavioral data is processed in real-time and never stored.

Code Changes

Client-Side (HTML)

Before (reCAPTCHA)

index.html
<!-- reCAPTCHA -->
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<form action="/submit" method="POST">
  <div class="g-recaptcha" data-sitekey="YOUR_SITE_KEY"></div>
  <button type="submit">Submit</button>
</form>

After (gkCAPTCHA)

index.html
<script src="https://gkcaptcha.gatekeeper.sa/widget/gk-captcha.js" async defer></script>

<form action="/submit" method="POST">
  <!-- your form fields -->
  <gk-captcha sitekey="pk_live_..."></gk-captcha>
  <button type="submit">Submit</button>
</form>

Server-Side (Verification)

Before (reCAPTCHA)

server.ts
// reCAPTCHA server-side verification
const response = await fetch('https://www.google.com/recaptcha/api/siteverify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    secret: process.env.RECAPTCHA_SECRET_KEY,
    response: req.body['g-recaptcha-response'],
  }),
})
const data = await response.json()
// { success: bool, challenge_ts, hostname, "error-codes": [] }

After (gkCAPTCHA)

server.js
// On your server (Node.js). Never send your secret key to the browser.
// The form arrives URL-encoded: app.use(express.urlencoded({ extended: false }))
const response = await fetch('https://gkcaptcha.gatekeeper.sa/api/v1/token/verify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    siteKey: process.env.GKCAPTCHA_SITE_KEY,
    secret: process.env.GKCAPTCHA_SECRET_KEY,
    token: req.body['gk-captcha-token'],
  }),
});
const result = await response.json();
// { success, score, timestamp, decision?, error?, reasonCode? }
// success only means the token is authentic. A "block" decision must be rejected.
if (result.success && result.decision !== 'block') {
  // Passed: process the form.
} else {
  // Failed or blocked: reject the request.
}

If you used explicit rendering

reCAPTCHA lets you create widgets from JavaScript with render() and callbacks. gkCAPTCHA has no global object: the gk-captcha element is the whole API. Find each call you use below.

reCAPTCHAgkCAPTCHANotes
grecaptcha.render(container, params)document.createElement('gk-captcha')Create the element, set sitekey, and add it to the page. Full example below.
callbackcaptcha-verifiedAn event on the element. The token is in e.detail.token.
expired-callbackcaptcha-expiredAn event on the element. No token is kept after it fires.
error-callbackcaptcha-errorAn event on the element. The reason is in e.detail.code.
grecaptcha.reset(id)el.reset()Call the method on the element itself. There is no widget ID.
grecaptcha.execute(id)el.execute()Call the method on the element itself. There is no widget ID.
grecaptcha.getResponse(id)await el.getToken()getToken() returns a promise. It waits for verification and never returns an expired token.
grecaptcha.ready(cb)?onload=customElements.whenDefined('gk-captcha')The element works whenever the script loads, so no onload callback is needed. Wait for customElements.whenDefined('gk-captcha') before you call a method.
g-recaptcha-responsegk-captcha-tokenRead this field name on your server instead.
data-themedata-sizethemesizePlain attributes with no data- prefix. For an invisible widget, use mode="invisible".
hllangA plain attribute. See the 21 supported languages on the widget page.
app.js
const form = document.querySelector('#signup-form');
const captcha = document.createElement('gk-captcha');
captcha.setAttribute('sitekey', 'pk_live_...');
captcha.setAttribute('mode', 'invisible');
form.appendChild(captcha);

form.addEventListener('submit', async (event) => {
  event.preventDefault();
  try {
    await customElements.whenDefined('gk-captcha');
    await captcha.getToken(); // also fills the gk-captcha-token field
    // URL-encoded, so a standard express.urlencoded() server reads req.body['gk-captcha-token'].
    await fetch(form.action, { method: 'POST', body: new URLSearchParams(new FormData(form)) });
  } catch (error) {
    // Show an error message. Never send the form without a token.
  } finally {
    captcha.reset(); // each token works once
  }
});

Step-by-Step Migration

  1. Sign up at gatekeeper.sa and create a site to get your site key (pk_live_...) and secret key (sk_live_...).
  2. Remove the reCAPTCHA script tag: remove the script loading google.com/recaptcha/api.js.
  3. Add the gkCAPTCHA script tag: <script src="https://gkcaptcha.gatekeeper.sa/widget/gk-captcha.js" async defer></script>
  4. Replace the widget element: change <div class="g-recaptcha" ...> to <gk-captcha sitekey="pk_live_...">.
  5. Update server-side verification: change the endpoint from google.com/recaptcha/api/siteverify to gkcaptcha.gatekeeper.sa/api/v1/token/verify. Send a JSON body with siteKey, secret and token, and read the token from the gk-captcha-token field.
  6. Update environment variables: rename RECAPTCHA_SECRET_KEY to GKCAPTCHA_SECRET_KEY and RECAPTCHA_SITE_KEY to GKCAPTCHA_SITE_KEY.
  7. Test in staging: verify the widget renders, verification succeeds, and your form submission flow works end-to-end.

Testing Checklist

  • Widget renders on the page
  • Verification succeeds with valid interaction
  • Verification rejects without interaction
  • Error handling works when API is unreachable
  • Form submission flow works end-to-end
  • Mobile layout renders correctly