Get your keys
Add your website in your free account. You get a site key (public, goes in the page) and a secret (stays on your server).
Add a website →Add the widget in two lines, verify the token on your server, and move from hCaptcha or reCAPTCHA by changing the keys.
https://fitmykb.comPOST /captcha/siteverifyGet started · 5 minutes
Add your website in your free account. You get a site key (public, goes in the page) and a secret (stays on your server).
Add a website →One script, and one div inside your form, where the box should appear.
Before you accept the form, send its fmk-captcha-response to siteverify with your secret.
<script src="https://fitmykb.com/captcha/api.js" async defer></script> <form method="post" action="/signup"> <input name="email" type="email"> <div class="fmk-captcha" data-sitekey="YOUR_SITE_KEY"></div> <button>Sign up</button> </form>
When the visitor passes, the widget puts a one-time token in the form as fmk-captcha-response, and also as h-captcha-response and g-recaptcha-response so code written for hCaptcha or reCAPTCHA keeps working.
Get started
One tick for people (or nothing at all in invisible mode). Every browser does the hidden work; visitors who look automated get a quick puzzle. About 1 second. Best for sign-up, login, contact, comments and newsletters.
Every visitor solves a quick puzzle drawn just for them, after the same hidden work: slide a piece into a gap, turn a picture upright, tap shapes in order, or select every picture with a tree, car or boat (like reCAPTCHA's grid), or a mix. About 3 to 5 seconds. Best for payments, password reset, OTP requests and anything bots target hard.
Choose the type for each website in your dashboard; it's enforced on our side, so a bot can't skip it, and switching needs no code change. A page can also ask for the puzzle with data-challenge="puzzle" (it can only make the check stronger, never weaker). Both types are in every plan. Try both and get a suggestion for your form →
Get started
Every website you add starts in test mode: its keys work straight away, with no approval, on localhost, 127.0.0.1, private network addresses and test domains (*.local, *.test, *.localhost). siteverify adds "test": true to its replies, so your code can tell.
When you're ready, press Request live on the website in your dashboard. We check the website and switch the same keys on for your real domain, usually within a working day; nothing in your code changes.
Your server
https://fitmykb.com/captcha/siteverify| Parameter | What it is | |
|---|---|---|
secret | required | your site's secret. Keep it in an environment variable, never in a page or app |
response | required | the token from the form's fmk-captcha-response field |
sitekey | optional | refuse tokens made with a different site key |
Send it as a form (application/x-www-form-urlencoded) or as JSON. The reply is JSON: see the fields.
curl https://fitmykb.com/captcha/siteverify \ -d secret="$FMK_SECRET" \ -d response="$TOKEN"
$reply = json_decode(file_get_contents('https://fitmykb.com/captcha/siteverify', false, stream_context_create(['http' => [
'method' => 'POST', 'timeout' => 5,
'header' => 'Content-Type: application/x-www-form-urlencoded',
'content' => http_build_query(['secret' => getenv('FMK_SECRET'), 'response' => $_POST['fmk-captcha-response'] ?? '']),
]])), true);
if (empty($reply['success'])) { http_response_code(400); exit('Please confirm you are human.'); }const reply = await fetch("https://fitmykb.com/captcha/siteverify", {
method: "POST",
body: new URLSearchParams({ secret: process.env.FMK_SECRET, response: req.body["fmk-captcha-response"] ?? "" }),
signal: AbortSignal.timeout(5000),
}).then((r) => r.json());
if (!reply.success) return res.status(400).send("Please confirm you are human.");import os, requests
reply = requests.post("https://fitmykb.com/captcha/siteverify", timeout=5, data={
"secret": os.environ["FMK_SECRET"], "response": request.form.get("fmk-captcha-response", "")}).json()
if not reply.get("success"):
abort(400, "Please confirm you are human.")var body = "secret=" + URLEncoder.encode(System.getenv("FMK_SECRET"), UTF_8)
+ "&response=" + URLEncoder.encode(token, UTF_8);
var request = HttpRequest.newBuilder(URI.create("https://fitmykb.com/captcha/siteverify"))
.timeout(Duration.ofSeconds(5))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(body)).build();
var json = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()).body();
boolean ok = json.contains("\"success\":true"); // or parse it with your JSON libraryclient := &http.Client{Timeout: 5 * time.Second}
resp, err := client.PostForm("https://fitmykb.com/captcha/siteverify", url.Values{
"secret": {os.Getenv("FMK_SECRET")}, "response": {r.FormValue("fmk-captcha-response")}})
if err != nil { /* see "If FitMyKB can't be reached" */ }
defer resp.Body.Close()
var reply struct{ Success bool `json:"success"` }
json.NewDecoder(resp.Body).Decode(&reply)using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
var reply = await http.PostAsync("https://fitmykb.com/captcha/siteverify", new FormUrlEncodedContent(new Dictionary<string, string> {
["secret"] = Environment.GetEnvironmentVariable("FMK_SECRET"), ["response"] = Request.Form["fmk-captcha-response"] }));
var ok = JsonDocument.Parse(await reply.Content.ReadAsStringAsync()).RootElement.GetProperty("success").GetBoolean();require "net/http"; require "json"
reply = JSON.parse(Net::HTTP.post_form(URI("https://fitmykb.com/captcha/siteverify"),
"secret" => ENV["FMK_SECRET"], "response" => params["fmk-captcha-response"].to_s).body)
halt 400, "Please confirm you are human." unless reply["success"]val reply = HttpClient.newHttpClient().send(
HttpRequest.newBuilder(URI("https://fitmykb.com/captcha/siteverify"))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString("secret=${System.getenv("FMK_SECRET")}&response=$token")).build(),
HttpResponse.BodyHandlers.ofString()).body()
if (!JSONObject(reply).getBoolean("success")) throw ResponseStatusException(HttpStatus.BAD_REQUEST)struct Reply: Content { let success: Bool; let score: Double? }
let res = try await req.client.post("https://fitmykb.com/captcha/siteverify") { r in
try r.content.encode(["secret": Environment.get("FMK_SECRET")!, "response": token], as: .urlEncodedForm)
}
guard try res.content.decode(Reply.self).success else { throw Abort(.badRequest) }let reply: serde_json::Value = reqwest::Client::new()
.post("https://fitmykb.com/captcha/siteverify")
.form(&[("secret", std::env::var("FMK_SECRET")?), ("response", token)])
.timeout(std::time::Duration::from_secs(5)).send().await?.json().await?;
if reply["success"] != true { return Err(StatusCode::BAD_REQUEST.into()); }final res = await http.post(Uri.parse("https://fitmykb.com/captcha/siteverify"),
body: {"secret": Platform.environment["FMK_SECRET"]!, "response": token});
if (jsonDecode(res.body)["success"] != true) return Response(400);{:ok, %{body: reply}} = Req.post("https://fitmykb.com/captcha/siteverify",
form: [secret: System.fetch_env!("FMK_SECRET"), response: token])
unless reply["success"], do: raise "Please confirm you are human."const reply = await (await fetch("https://fitmykb.com/captcha/siteverify", { method: "POST",
body: new URLSearchParams({ secret: Deno.env.get("FMK_SECRET")!, response: token }) })).json();
if (!reply.success) return new Response("Please confirm you are human.", { status: 400 });Your server
{
"success": true,
"challenge_ts": "2026-10-17T10:15:02+00:00",
"hostname": "example.com",
"score": 0.9,
"credit": false
}
| Field | Meaning |
|---|---|
success | true only for a valid, unused token from this site, less than 2 minutes old |
hostname | the website the widget was on: check it's yours |
score | 0 to 1: how sure we are it was a person (at least 0.6 after a solved puzzle). Ask for more, such as an email code, below a threshold you choose |
action | score-only checks: the action the page named, such as login |
challenge_ts | when the visitor passed (ISO 8601, UTC) |
over_quota | present and true when your account is over its monthly plan. Verification keeps working |
error-codes | on failure: missing-input-secret, invalid-input-secret, missing-input-response, invalid-or-already-seen-response, expired-input-response, sitekey-secret-mismatch |
Your server
data-error-callback.The widget
| Attribute | Values |
|---|---|
data-sitekey | your site key (required) |
data-challenge | puzzle: every visitor gets the puzzle on this page (the website's type in your dashboard always applies too) |
data-puzzle | slide, rotate, tap, grid or mixed: the puzzle style on this page (default: the website's style in your dashboard). Tap needs a pointer; slide, rotate and grid also work with a keyboard |
data-size | normal (304×78), compact (166×146) or invisible |
data-theme | auto (follows the visitor), light or dark |
data-hl | en, hi, ta, te, bn, mr, kn, gu, pa, ml, or, ne, ur, es, fr, de, pt, it, nl, pl, tr, ru, uk, ar, fa, he, zh, ja, ko, id, ms, vi, th, fil, sw (35 languages; ar, fa, he and ur read right to left). Default: the page's lang. |
data-callback | name of a function called with the token when the visitor passes |
data-expired-callback | called when the token expires (after 2 minutes) before the form was sent |
data-error-callback, data-open-callback, data-close-callback | on a network error; when a puzzle opens; when it closes without being solved |
The widget
Load with api.js?render=explicit&onload=myInit to place widgets yourself. Everything is on window.fmkcaptcha (and on window.hcaptcha when hCaptcha isn't on the page).
const id = fmkcaptcha.render("captcha-box", { sitekey: "YOUR_SITE_KEY", theme: "dark", callback: (token) => {} });
fmkcaptcha.getResponse(id); // the token, or ""
fmkcaptcha.reset(id); // after a failed submit: start again
fmkcaptcha.execute(id); // invisible mode: run the check now
fmkcaptcha.remove(id);
The widget
No box: run the check when the visitor submits. Most people never see anything; a suspicious visitor gets a quick puzzle in a small popup.
<div class="fmk-captcha" data-sitekey="YOUR_SITE_KEY" data-size="invisible" data-callback="onHuman"></div>
<script>
form.addEventListener("submit", (e) => { if (!fmkcaptcha.getResponse()) { e.preventDefault(); fmkcaptcha.execute(); } });
function onHuman() { form.submit(); }
</script>
The widget
No box and no puzzle, ever: the check runs when your page asks, and your server decides from the score. Name each action, so you can see what was protected.
<script src="https://fitmykb.com/captcha/api.js?render=YOUR_SITE_KEY"></script>
<script>
fmkcaptcha.ready(() => {
fmkcaptcha.execute("YOUR_SITE_KEY", { action: "login" }).then((token) => {
// send the token with the form, then check it on your server
});
});
</script>
siteverify then answers with "score" (0 to 1) and "action": "login". Accept above a threshold you choose (0.5 is a good start), and ask for more below it, such as an email code. Code written for reCAPTCHA v3 keeps working: grecaptcha.ready and grecaptcha.execute are available when reCAPTCHA isn't on the page. A website set to the Puzzle type still gets its puzzle.
The widget
If your site sends a CSP, allow our script and frame:
script-src 'self' https://fitmykb.com; frame-src https://fitmykb.com;
Guides
function Captcha({ sitekey, onToken }) {
const box = useRef(null);
useEffect(() => {
const start = () => { const id = window.fmkcaptcha.render(box.current, { sitekey, callback: onToken }); return () => window.fmkcaptcha.remove(id); };
if (window.fmkcaptcha) return start();
const s = Object.assign(document.createElement("script"), { src: "https://fitmykb.com/captcha/api.js?render=explicit", async: true });
let stop; s.onload = () => { stop = start(); }; document.head.append(s);
return () => stop?.();
}, [sitekey]);
return <div ref={box} />;
}add_action('wp_enqueue_scripts', fn() => wp_enqueue_script('fmk-captcha', 'https://fitmykb.com/captcha/api.js', [], null, true));
// In your form's HTML: <div class="fmk-captcha" data-sitekey="YOUR_SITE_KEY"></div>
// Then check $_POST['fmk-captcha-response'] with the PHP code above before saving the form.$request->validate(['fmk-captcha-response' => ['required', function ($attribute, $value, $fail) {
$ok = Http::asForm()->timeout(5)->post('https://fitmykb.com/captcha/siteverify', [
'secret' => env('FMK_SECRET'), 'response' => $value])->json('success');
if (!$ok) $fail('Please confirm you are human.');
}]]);def human(request):
reply = requests.post("https://fitmykb.com/captcha/siteverify", timeout=5, data={
"secret": settings.FMK_SECRET, "response": request.POST.get("fmk-captcha-response", "")}).json()
return reply.get("success", False)<div ref="box"></div>
onMounted(() => fmkcaptcha.render(box.value, { sitekey: "YOUR_SITE_KEY", callback: (t) => (token.value = t) }));@ViewChild("box") box!: ElementRef;
ngAfterViewInit() { (window as any).fmkcaptcha.render(this.box.nativeElement, { sitekey: "YOUR_SITE_KEY", callback: (t: string) => this.token = t }); }<div bind:this={box}></div>
onMount(() => fmkcaptcha.render(box, { sitekey: "YOUR_SITE_KEY", callback: (t) => (token = t) }));Apps
The SDKs open the check in a small native sheet and hand your app a one-time token. Send it to your server, which checks it with siteverify exactly as for a website. Never put your secret in an app.
com.example.shop) as one of the site key's websites. Language, theme, size, puzzle style and score-only mode work as on the web, in all 35 languages.// settings.gradle: include(":fitmykb-verify") from sdk/android
FitMyKBVerify.verify(this, FitMyKBVerify.Config(siteKey = "YOUR_SITE_KEY")) { result ->
when (result) {
is FitMyKBVerify.Result.Success -> api.signUp(email, password, captcha = result.token)
is FitMyKBVerify.Result.Failure -> showError(result.code)
FitMyKBVerify.Result.Cancelled -> Unit
}
}FitMyKBVerify.INSTANCE.verify(this, new FitMyKBVerify.Config("YOUR_SITE_KEY", "en", "auto", "normal", null, null, "https://fitmykb.com"), result -> {
if (result instanceof FitMyKBVerify.Result.Success) {
api.signUp(email, password, ((FitMyKBVerify.Result.Success) result).getToken());
}
return kotlin.Unit.INSTANCE;
});// Swift Package Manager: add sdk/ios
import FitMyKBVerify
FitMyKBVerify.verify(from: self, config: .init(siteKey: "YOUR_SITE_KEY")) { result in
switch result {
case .success(let token): api.signUp(email: email, password: password, captcha: token)
case .failure(let code): showError(code)
case .cancelled: break
}
}Button("Create account") {
guard let root = UIApplication.shared.connectedScenes.compactMap({ ($0 as? UIWindowScene)?.keyWindow?.rootViewController }).first else { return }
FitMyKBVerify.verify(from: root, config: .init(siteKey: "YOUR_SITE_KEY", size: "invisible")) { result in
if case .success(let token) = result { Task { await model.signUp(captcha: token) } }
}
}# pubspec.yaml: fitmykb_verify (path: sdk/flutter) final token = await FitMyKBVerify.verify(context, siteKey: 'YOUR_SITE_KEY', appId: 'com.example.shop'); if (token != null) await api.signUp(email, password, captcha: token);
// npm install ./sdk/react-native react-native-webview
import FitMyKBVerify from "@fitmykb/verify-react-native";
<FitMyKBVerify siteKey="YOUR_SITE_KEY" appId="com.example.shop" language="hi" onToken={(token) => signUp(token)} />Open https://fitmykb.com/captcha/mobile?sitekey=YOUR_SITE_KEY&app=com.example.shop&hl=en
in a WebView. The page sends JSON messages: {"event": "ready" | "token" | "expired" | "error" | "close", "token"?}
to window.FitMyKBVerify (Android), webkit.messageHandlers.fitmykbVerify (iOS), ReactNativeWebView,
or the FitMyKBVerifyChannel JavaScript channel (Flutter).The SDK sources are open: download the SDKs (zip). They need nothing beyond the system WebView.
Guides
| hCaptcha | reCAPTCHA v2 | FitMyKB Verify | |
|---|---|---|---|
| Script | js.hcaptcha.com/1/api.js | google.com/recaptcha/api.js | https://fitmykb.com/captcha/api.js |
| Widget | class="h-captcha" | class="g-recaptcha" | class="fmk-captcha" (h-captcha also works) |
| Form field | h-captcha-response | g-recaptcha-response | all three are filled |
| Verify | api.hcaptcha.com/siteverify | google.com/recaptcha/api/siteverify | https://fitmykb.com/captcha/siteverify |
| Reply | the same fields: success, challenge_ts, hostname, error-codes; plus score | ||
Reference
over_quota: true.