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.
| hCaptcha | gkCAPTCHA | Notes |
|---|---|---|
hcaptcha.render(container, params) | document.createElement('gk-captcha') | Create the element, set sitekey, and add it to the page. Full example below. |
callback | captcha-verified | An event on the element. The token is in e.detail.token. |
expired-callback | captcha-expired | An event on the element. No token is kept after it fires. |
error-callback | captcha-error | An 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-response | gk-captcha-token | Read this field name on your server instead. |
data-themedata-size | themesize | Plain attributes with no data- prefix. For an invisible widget, use mode="invisible". |
hl | lang | A 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
- Sign up at gatekeeper.sa and create a site to get your site key (
pk_live_...) and secret key (sk_live_...). - Remove the hCaptcha script tag: remove the script loading
js.hcaptcha.com/1/api.js. - Add the gkCAPTCHA script tag:
<script src="https://gkcaptcha.gatekeeper.sa/widget/gk-captcha.js" async defer></script> - Replace the widget element: change
<div class="h-captcha" ...>to<gk-captcha sitekey="pk_live_...">. - Update server-side verification: change the endpoint from
hcaptcha.com/siteverifytogkcaptcha.gatekeeper.sa/api/v1/token/verify. Send a JSON body withsiteKey,secretandtoken, and read the token from thegk-captcha-tokenfield. - Update environment variables: rename
HCAPTCHA_SECRET_KEYtoGKCAPTCHA_SECRET_KEYandHCAPTCHA_SITE_KEYtoGKCAPTCHA_SITE_KEY. - 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