Create a site key
CAPTCHA works without a paid plan: link an email, then create a key in Captcha. Add the exact HTTP or HTTPS origins, including development ports, and choose the challenge types. Without a plan you get one key with Slider and Rotate; plans add more keys, origins and every challenge type.
The secret key is shown once, right after you create the key or rotate its secret. Store it in your server's environment straight away; if it is lost, rotate it.
CaptchaRender the widget
Place the widget inside your form with the public site key. When the check passes, the widget writes the token into the hidden captcha_token field, calls your callback and fires a hotwall-captcha-success event. Only the public site key belongs in the browser.
In the key settings, choose Checkbox (a box and a short puzzle) or Invisible (a background check, with a puzzle only when it is not sure). The widget speaks English, Russian, Chinese, Japanese and Kazakh; set data-lang to pick one.
<form method="post" action="/contact">
<!-- ... your fields ... -->
<div class="hotwall-captcha"
data-site-key="YOUR_SITE_KEY"
data-callback="onCaptchaSuccess"></div>
<!-- The widget writes the token here when the check passes. -->
<input type="hidden" name="captcha_token">
<button type="submit">Send</button>
</form>
<script>
window.onCaptchaSuccess = function (token) {
// Optional: enable the submit button, send the form with fetch, ...
};
</script>
<script src="https://captcha.hotwall.app/v1/widget.js" crossorigin="anonymous" async defer></script>Verify on the server
POST the token as JSON to /v1/siteverify with your secret in the X-Secret-Key header. Accept the form only when the HTTP response is successful, success is exactly true and hostname is your own site. The answer also tells you the challenge type, the mode and when the check was passed.
// Server only. Never expose this environment variable to the browser.
export async function verifyCaptcha(token) {
if (typeof token !== "string" || !token.trim()) return false;
const secret = process.env.HOTWALL_CAPTCHA_SECRET;
if (!secret) throw new Error("Missing CAPTCHA secret");
try {
const response = await fetch("https://captcha.hotwall.app/v1/siteverify", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Secret-Key": secret },
body: JSON.stringify({ token }),
signal: AbortSignal.timeout(5000)
});
if (!response.ok) return false;
const result = await response.json();
// A token is single-use and says where it was earned: check it was on your site.
return result.success === true && result.hostname === "www.example.com";
} catch {
return false;
}
}
// Process the form only if await verifyCaptcha(req.body.captcha_token) is true.Handle failures safely
Reject missing, expired, invalid or already used tokens. A network timeout or invalid verification response must not allow the form through. Request a new challenge after a failed verification; never log secret keys or full tokens.
If the widget reports an origin error, compare the scheme, hostname and port with Allowed origins. Rotate an exposed secret and update the backend. Test both a valid token and a deliberately invalid token before release.
Receive webhooks
On paid plans, a key can send passed, blocked and rejected checks to your HTTPS endpoint. Every request is signed with the key's secret and carries a timestamp: verify the signature over the raw body and reject old requests.
import crypto from "node:crypto";
// X-HotWall-Signature: t=<unix time>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with your secret key>
export function verifyCaptchaWebhook(rawBody, header, secret) {
const parts = Object.fromEntries(String(header).split(",").map((part) => part.split("=")));
if (!parts.t || !parts.v1 || Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
return expected.length === parts.v1.length
&& crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}