Skip to content
FitMyKBVerify

FitMyKB Verify developer docs

Add the widget in two lines, verify the token on your server, and move from hCaptcha or reCAPTCHA by changing the keys.

Base URL
https://fitmykb.com
Verify
POST /captcha/siteverify
Token
one use · 2 min
Works with
hCaptcha · reCAPTCHA

Get started · 5 minutes

Quickstart

1

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 →
2

Add the widget

One script, and one div inside your form, where the box should appear.

3

Check the token

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>
The box never changes size. When a visitor needs a puzzle, it opens in its own floating window next to the box (a bottom sheet on phones, centred for invisible checks), like reCAPTCHA's, so your page's layout never moves. Everything the widget adds is shielded from your site's CSS.

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.

Always check the token on your server. The widget alone proves nothing: a bot can skip it and post your form directly. A token works once, for 2 minutes.

Get started

Two types: smart checkbox and puzzle

Smart checkbox

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.

Puzzle

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

Test and live

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.

Test keys for automated testsKeys that always pass or always fail, for your CI. Sign in to see them.Sign in to see test keys

Your server

Verify the token: siteverify

POSThttps://fitmykb.com/captcha/siteverify
ParameterWhat it is
secretrequiredyour site's secret. Keep it in an environment variable, never in a page or app
responserequiredthe token from the form's fmk-captcha-response field
sitekeyoptionalrefuse 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
curl https://fitmykb.com/captcha/siteverify \
  -d secret="$FMK_SECRET" \
  -d response="$TOKEN"
PHP
$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.'); }
Node.js
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.");
Python
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.")
Java
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 library
Go
client := &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)
C# / .NET
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();
Ruby
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"]
Kotlin (Ktor / Spring)
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)
Swift (Vapor)
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) }
Rust
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()); }
Dart (server)
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);
Elixir
{: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."
Deno / Bun
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

The reply and errors

{
  "success": true,
  "challenge_ts": "2026-10-17T10:15:02+00:00",
  "hostname": "example.com",
  "score": 0.9,
  "credit": false
}
FieldMeaning
successtrue only for a valid, unused token from this site, less than 2 minutes old
hostnamethe website the widget was on: check it's yours
score0 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
actionscore-only checks: the action the page named, such as login
challenge_tswhen the visitor passed (ISO 8601, UTC)
over_quotapresent and true when your account is over its monthly plan. Verification keeps working
error-codeson failure: missing-input-secret, invalid-input-secret, missing-input-response, invalid-or-already-seen-response, expired-input-response, sitekey-secret-mismatch

Your server

If FitMyKB can't be reached

Always call siteverify with a timeout (5 seconds is plenty). If it times out, you choose: refuse the form (safest for sign-ups and logins), or accept it and mark it for review (kindest for contact and checkout forms). The widget itself also tells you with data-error-callback.

The widget

Widget options

AttributeValues
data-sitekeyyour site key (required)
data-challengepuzzle: every visitor gets the puzzle on this page (the website's type in your dashboard always applies too)
data-puzzleslide, 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-sizenormal (304×78), compact (166×146) or invisible
data-themeauto (follows the visitor), light or dark
data-hlen, 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-callbackname of a function called with the token when the visitor passes
data-expired-callbackcalled when the token expires (after 2 minutes) before the form was sent
data-error-callback, data-open-callback, data-close-callbackon a network error; when a puzzle opens; when it closes without being solved

The widget

JavaScript API

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

Invisible mode

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

Score only (like reCAPTCHA v3)

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

Content-Security-Policy

If your site sends a CSP, allow our script and frame:

script-src 'self' https://fitmykb.com;
frame-src https://fitmykb.com;

Guides

React, WordPress, Laravel, Django

React / Next.js
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} />;
}
WordPress (theme or a code-snippets plugin)
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.
Laravel
$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.');
}]]);
Django
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)
Vue
<div ref="box"></div>
onMounted(() => fmkcaptcha.render(box.value, { sitekey: "YOUR_SITE_KEY", callback: (t) => (token.value = t) }));
Angular
@ViewChild("box") box!: ElementRef;
ngAfterViewInit() { (window as any).fmkcaptcha.render(this.box.nativeElement, { sitekey: "YOUR_SITE_KEY", callback: (t: string) => this.token = t }); }
Svelte
<div bind:this={box}></div>
onMount(() => fmkcaptcha.render(box, { sitekey: "YOUR_SITE_KEY", callback: (t) => (token = t) }));

Apps

Android, iOS, Flutter and React Native

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.

One setting: in your dashboard, add the app's package name or bundle id (for example 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.
Android (Kotlin)
// 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
    }
}
Android (Java)
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;
});
iOS (Swift)
// 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
    }
}
iOS (SwiftUI)
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) } }
    }
}
Flutter
# 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);
React Native
// 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)} />
Any other app (WebView)
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

From hCaptcha or reCAPTCHA

hCaptchareCAPTCHA v2FitMyKB Verify
Scriptjs.hcaptcha.com/1/api.jsgoogle.com/recaptcha/api.jshttps://fitmykb.com/captcha/api.js
Widgetclass="h-captcha"class="g-recaptcha"class="fmk-captcha" (h-captcha also works)
Form fieldh-captcha-responseg-recaptcha-responseall three are filled
Verifyapi.hcaptcha.com/siteverifygoogle.com/recaptcha/api/siteverifyhttps://fitmykb.com/captcha/siteverify
Replythe same fields: success, challenge_ts, hostname, error-codes; plus score

Reference

Limits and privacy

  • Free: 10,000 verifications a month, 1 website. Paid plans: 250,000, 2,000,000 and 10,000,000. Plans.
  • Over the limit, verification keeps working and the reply carries over_quota: true.
  • The widget sets no cookies and stores no fingerprint. It counts pointer, key and touch events (not where or what), how long the box was shown, and whether the browser reports automation. IP addresses are kept, scrambled, in memory for up to 30 minutes to raise the work after failures.
  • Accessible: the box and the puzzle work with a keyboard (arrow keys move the slider) and screen readers.
Ready to stop the bots?Free for 10,000 checks a month, no card needed. Starter from ₹99.
Get your free keys