Start in Observe on one route. Use the deployment URL and installed package version supplied for your workspace. Historical host examples and source capabilities do not establish a live service or published release.
Warning
Use the unpublished reviewed kit candidate for these server examples, not npm
0.3.0. Normal protected browser actions require hosted live Gate authority.
Legacy CAPTCHA shims preserve callback/form compatibility only during bounded
migration; they are not production browser authority. Hidden response fields
alone cannot replace cookie/request proof. Complete the first-party hosted
rollout before removing the previous production control.
Migration Approaches
| Approach | Effort | Description |
|---|---|---|
| Clean migration | Medium | Remove hCaptcha entirely, integrate MANDATE directly |
| Shim migration | Low | Drop-in compatibility layer that maps hCaptcha APIs to MANDATE |
Clean Migration
Remove the hCaptcha Script and Widget
<!-- Remove these -->
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<div class="h-captcha" data-sitekey="YOUR_SITE_KEY"></div>Add the MANDATE Script
<script async src="https://gate.x-lock.dev/g.js?k=YOUR_SITE_KEY"></script>Update Frontend Token Retrieval
hcaptcha.execute({ async: true }).then(({ response }) => {
document.getElementById("token-field").value = response;
});await fetch("/api/action", { method: "POST" });
// Production same-origin protected requests carry Mndt-Sess automatically.See Frontend Integration for the same-origin request-boundary contract.
Update Backend Verification
Replace the hCaptcha siteverify API call:
const response = await fetch("https://api.hcaptcha.com/siteverify", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
secret: HCAPTCHA_SECRET,
response: token,
}),
});
const data = await response.json();
if (!data.success) {
// Block request
}import { validateHostedRequest } from "@mandate-security/node";
const result = await validateHostedRequest(req, {
siteSecret: process.env.MANDATE_SITE_SECRET,
siteKey: process.env.MANDATE_SITE_KEY,
apiKey: process.env.MANDATE_API_KEY,
gateUrl: process.env.MANDATE_GATE_URL,
credentialMode: "cookie", credentialCookieName: "Mndt-Sess",
highValue: true, origin: "https://app.example.com",
});
if (!result.success) {
// Keep the protected action denied. Prefer a framework adapter so
// authenticated cookie/companion/native updates are applied too.
}Clean Up
- Remove hCaptcha secret key from environment variables
- Remove
.h-captchacontainer elements and related CSS - Remove hCaptcha type definitions if using TypeScript
- Remove any
hcaptchaevent listeners (verified,expired,error)
Shim Migration
Swap Script Tags
<!-- Remove this -->
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<!-- Add these (order matters) -->
<script async src="https://gate.x-lock.dev/g.js?k=YOUR_SITE_KEY"></script>
<script src="https://gate.x-lock.dev/compat/mandate-shim-core.js"></script>
<script src="https://gate.x-lock.dev/compat/hcaptcha-shim.js"></script>Info
Shims are served from the gate at /compat/*. Load order: g.js → mandate-shim-core.js → provider shim. The core reads the legacy compatibility bridge only and never calls polymorphic g.js globals. Enable MANDATE_TOKEN_BRIDGE=legacy only in non-production shim migration tests; production rejects page-visible token bridges.
Remove Widget Containers
<!-- Can be removed (shim handles render calls as no-ops) -->
<div class="h-captcha" data-sitekey="YOUR_SITE_KEY"></div>Verify Existing Code Works
The shim provides the following hCaptcha API surface:
| hCaptcha API | Shim Behavior |
|---|---|
hcaptcha.execute(options) | Returns MANDATE token via Promise |
hcaptcha.getResponse(widgetId) | Returns current MANDATE token |
hcaptcha.getRespKey(widgetId) | Returns token (same as getResponse) |
hcaptcha.render(container, options) | No-op, returns widget ID |
hcaptcha.remove(widgetId) | No-op |
hcaptcha.reset(widgetId) | Clears hidden form fields |
Update Backend Verification
Replace the hCaptcha siteverify call with MANDATE SDK:
import { validateHostedRequest } from "@mandate-security/node";
const result = await validateHostedRequest(req, {
siteSecret: process.env.MANDATE_SITE_SECRET,
siteKey: process.env.MANDATE_SITE_KEY,
apiKey: process.env.MANDATE_API_KEY,
gateUrl: process.env.MANDATE_GATE_URL,
credentialMode: "cookie", credentialCookieName: "Mndt-Sess",
highValue: true, origin: "https://app.example.com",
});
if (!result.success) {
// Keep the protected action denied. Prefer a framework adapter so
// authenticated cookie/companion/native updates are applied too.
}Info
The shim populates hidden form fields named h-captcha-response and g-recaptcha-response (hCaptcha's legacy field name) for compatibility with existing form submissions.
Remove the Shim
Once migration is confirmed stable, remove the shim scripts, disable MANDATE_TOKEN_BRIDGE=legacy, and move production browser routes to first-party cookie delivery and hosted validation (see Frontend Integration).
Key Differences
| Feature | hCaptcha | MANDATE |
|---|---|---|
| User interaction | Yes (puzzle challenges) | Never |
| Backend verification | API call to hCaptcha | Local cryptographic check |
| Token delivery | Hidden form field or callback | HttpOnly Mndt-Sess in production; X-Mndt only for compatibility |
| Latency | Network round-trip + user solve time | No MANDATE round trip for compatible local verification |
| Accessibility | Requires accessible challenge fallback | No interaction needed |
| Privacy | Data sent to hCaptcha servers | No third-party data sharing |
Tip
Since MANDATE has no user-facing interaction, you can remove all accessibility accommodations specific to CAPTCHA challenges (aria labels, skip links, challenge containers).
Warning
hCaptcha Enterprise features like hcaptcha.execute({ async: true, rqdata: "..." }) with custom rqdata are not supported by the shim. Use the clean migration approach for Enterprise deployments.