DEVELOPER DOCUMENTATION

Migrate from hCaptcha

Replace hCaptcha with MANDATE bot detection.

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

ApproachEffortDescription
Clean migrationMediumRemove hCaptcha entirely, integrate MANDATE directly
Shim migrationLowDrop-in compatibility layer that maps hCaptcha APIs to MANDATE

Clean Migration

Remove the hCaptcha Script and Widget

html
<!-- 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

html
<script async src="https://gate.x-lock.dev/g.js?k=YOUR_SITE_KEY"></script>

Update Frontend Token Retrieval

javascript Before (hCaptcha)
hcaptcha.execute({ async: true }).then(({ response }) => {
  document.getElementById("token-field").value = response;
});
javascript After (MANDATE)
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:

javascript Before (hCaptcha)
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
}
javascript After (MANDATE)
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-captcha container elements and related CSS
  • Remove hCaptcha type definitions if using TypeScript
  • Remove any hcaptcha event listeners (verified, expired, error)

Shim Migration

Swap Script Tags

html
<!-- 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

html
<!-- 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 APIShim 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:

javascript
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

FeaturehCaptchaMANDATE
User interactionYes (puzzle challenges)Never
Backend verificationAPI call to hCaptchaLocal cryptographic check
Token deliveryHidden form field or callbackHttpOnly Mndt-Sess in production; X-Mndt only for compatibility
LatencyNetwork round-trip + user solve timeNo MANDATE round trip for compatible local verification
AccessibilityRequires accessible challenge fallbackNo interaction needed
PrivacyData sent to hCaptcha serversNo 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.