Switch from hCaptcha

Migrate from hCaptcha to gkCAPTCHA in under 15 minutes.

Why Switch?

  • PDPL compliance: gkCAPTCHA is fully compliant with Saudi Arabia's Personal Data Protection Law. hCaptcha processes data on US-based infrastructure, 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 (hCaptcha)

index.html
<!-- hCaptcha -->
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<form action="/submit" method="POST">
  <div class="h-captcha" 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 (hCaptcha)

server.ts
// hCaptcha server-side verification
const response = await fetch('https://hcaptcha.com/siteverify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    secret: process.env.HCAPTCHA_SECRET_KEY,
    response: req.body['h-captcha-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

hCaptcha 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.

hCaptchagkCAPTCHANotes
hcaptcha.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.
hcaptcha.reset(id)el.reset()Call the method on the element itself. There is no widget ID.
hcaptcha.execute(id)el.execute()Call the method on the element itself. There is no widget ID.
hcaptcha.remove(id)el.remove()Call the method on the element itself. There is no widget ID.
hcaptcha.getResponse(id)await el.getToken()getToken() returns a promise. It waits for verification and never returns an expired token.
?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.
h-captcha-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 hCaptcha script tag: remove the script loading js.hcaptcha.com/1/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="h-captcha" ...> to <gk-captcha sitekey="pk_live_...">.
  5. Update server-side verification: change the endpoint from hcaptcha.com/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 HCAPTCHA_SECRET_KEY to GKCAPTCHA_SECRET_KEY and HCAPTCHA_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