///
How to Implement CAPTCHA on Your Website: A Step-by-Step Guide
Vladlen Vlasov
Vladlen Vlasov
Technical Writer
March 5, 2026
16 min
Please review the terms of use for the materials on this website.

How to Implement CAPTCHA on Your Website: A Step-by-Step Guide

generated-image (20).png

Bots are already part of your traffic — whether you see them or not. They create fake accounts, test stolen passwords, and quietly stress your backend long before anyone yells "incident." Understanding how to implement captcha on your website isn't just a nice-to-have; it's the difference between treating abuse as an afterthought and baking protection directly into your forms, flows, and infrastructure.

This guide walks you through two production-ready implementations: GeeTest CAPTCHA v3 and Cloudflare Turnstile. Along the way you'll also learn how to get captcha code (site keys and credentials), when a custom captcha makes sense, how to block captcha-bypass attempts, and how to validate your integration using CapMonster Cloud.

robots
Get started now and automate your solution reCAPTCHA v2

What Is CAPTCHA and Why Your Website Needs It

CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart) is a challenge-response mechanism that attempts to distinguish real users from automated scripts. Modern solutions range from traditional distorted-text puzzles to invisible behavioral analysis and interactive puzzle challenges like GeeTest's slider.

Without a CAPTCHA layer, any public form on your site is exposed to:

  • Credential stuffing — automated login attempts using leaked username/password pairs, commonly seen in abuse traffic.

  • Spam submissions — bots flooding contact forms, comment sections, or review pages.

  • Account enumeration — scripts probing for valid email addresses or usernames.

  • Scraping — automated extraction of pricing, inventory, or proprietary content.

The trade-off is always security vs. user experience: friction stops bots, but too much friction pushes real users away. Cloudflare Turnstile and GeeTest v3 are both modern options that aim to balance these aspects: Turnstile leans toward low-friction verification, while GeeTest v3 uses an interactive puzzle of different types with behavior-based checks to detect bots.

How to Get Your CAPTCHA Code (Keys & Credentials)

Before writing any integration code, you need provider-specific credentials.

GeeTest V3

  1. Create an account in the GeeTest dashboard.

  2. Go to the Captcha Dashboard and select CAPTCHA v3:

image.png

  1. Navigate to the section where you can create a new CAPTCHA and register your site (you typically specify domains and product type).

  2. After setup, you obtain a CAPTCHA ID (often referred to as gt) and a private Key for server-side communication.

  3. Store these values securely (for example, in environment variables) so your application can access them without hardcoding secrets.

Cloudflare Turnstile

  1. Log in to dash.cloudflare.com and open the Application security > Turnstile section.

image.png

  1. Click Add widget, enter your domain, and choose a widget mode: ManagedNon-interactive, or Invisible.

  2. Set Pre-clearance to Yes if the site is through Cloudflare Proxy (to avoid repeating the CAPTCHA).

  3. Once created, Cloudflare provides a Site Key (public, used client-side) and a Secret Key (private, used server-side).

  4. Use the Site Key in your HTML or JavaScript, and send the Secret Key only from your backend when calling the Turnstile verification API.

How to Implement GeeTest CAPTCHA v3: Step by Step

GeeTest v3 uses a multi-step flow: your server fetches a fresh challenge from GeeTest, the client renders the puzzle using that challenge (GeeTest API1), and your server verifies the solved tokens (GeeTest API2). Below is an example PHP + JavaScript implementation illustrating this pattern; adapt it to GeeTest's official SDK and your chosen language.

Step 1 — Server-Side Registration Endpoint

Create a PHP script (for example, geetest_register.php) that obtains a fresh challenge from GeeTest and passes it to the client:

<?php
header('Content-Type: application/json');
const CAPTCHA_ID = '07df3141a35**********19a473d7c50';
const CAPTCHA_KEY = '543b19036ef********8e07d121b81e9';
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
function getJson($url) {
    $res = @file_get_contents($url);
    return $res ? json_decode($res, true) : null;
}
// API1: Initialization
if ($path === '/register') {
    $data = getJson("https://api.geetest.com/register.php?gt=" . CAPTCHA_ID . "&json_format=1");
    echo json_encode($data ? [
        'gt' => CAPTCHA_ID,
        'challenge' => $data['challenge'],
        'success' => $data['success'] === 1,
        'new_captcha' => true
    ] : ['success' => 0]);
    exit;
}
// API2: Verification
if ($path === '/validate' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $req = json_decode(file_get_contents('php://input'), true);
    $data = getJson("https://api.geetest.com/validate.php?" . http_build_query([
        'seccode' => $req['geetest_seccode'] ?? '',
        'challenge' => $req['geetest_challenge'] ?? '',
        'gt' => CAPTCHA_ID,
        'json_format' => 1
    ]));
    echo json_encode(['success' => !empty($data['seccode'])]);
    exit;
}
http_response_code(404);
echo json_encode(['error' => 'Not found']);
?>

Step 2 — Client-Side Initialization

Load the GeeTest SDK and call initGeetest passing the parameters from the server (API1). Example using ajax:

ajax({
    url: "https://example.com/register",
    type: "get",
    dataType: "json",
    success: function (data) {
        initGeetest({
            gt: data.gt,
            challenge: data.challenge,
            offline: !data.success,
            new_captcha: true
        }, function (captchaObj) {
            captchaObj.appendTo("#captcha");
            captchaObj.onSuccess(function () {
                const result = captchaObj.getValidate();
                ajax({
                    url: "https://example.com/validate",
                    type: "post",
                    contentType: "application/json",
                    data: JSON.stringify(result),
                    success: function(res) {
                        if (res.success) alert('CAPTCHA passed');
                        else alert('CAPTCHA failed');
                    }
                });
            });
        });
    }
});

Checking the operation

Make sure that:

  • /register returns challenge

  • The captcha is displayed correctly

  • After passing the captcha, a request to /validate is visible in the browser console

  • The server returns "success": true

 

Failback (fallback mode)

If the GeeTest server is unavailable:

  • The client receives success: false

  • The captcha switches to local mode (works without connecting to GeeTest Cloud). To test this, simply substitute an incorrect CAPTCHA_ID (for example, 123456789).

Note: GeeTest also provides official server-side SDKs for different platforms and languages — you can choose the one that matches your technology stack. You can learn more in the official GeeTest CAPTCHA v3 documentation.

How to Implement Cloudflare Turnstile: Step by Step

Cloudflare Turnstile has no separate server-side "registration call." You embed a widget in your page, let it generate a token, and then verify that token server-side using Cloudflare's Siteverify API.

Step 1 — Connect the Turnstile script

Automatic rendering (widget is created automatically when the page loads):

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

Programmatic control (you create the widget yourself via JavaScript):

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" defer></script>

Important: the script must be loaded from the exact URL. Proxy or cache may cause failures.

Step 2 — Create a container for the widget

Auto:

<div class="cf-turnstile" data-sitekey="<YOUR_SITEKEY>"></div>

Programmatically:

<div id="turnstile-container"></div>

Step 3 — Widget configuration

Via data attributes:

<div class="cf-turnstile" data-sitekey="<YOUR_SITEKEY>" data-theme="light" data-size="normal" data-callback="onSuccess"> </div>

Via JavaScript:

const widgetId = turnstile.render("#turnstile-container", {
    sitekey: "<YOUR_SITEKEY>",
    theme: "light",
    size: "normal",
    callback: token => console.log("Token:", token)
});

Step 4 — Working with tokens

const token = turnstile.getResponse(widgetId);      // get token
const isExpired = turnstile.isExpired(widgetId);    // check expiration
turnstile.reset(widgetId);                          // reset
turnstile.remove(widgetId);                         // remove
turnstile.execute("#turnstile-container");         // manual execution

Step 5 — Integration with form

<form id="my-form" method="POST"> <input type="hidden" name="cf-turnstile-response" id="cf-turnstile-response"> <button type="submit">Submit</button> </form>
<script>
    function onSuccess(token) {
      document.getElementById("cf-turnstile-response").value = token;
    }
</script>

Full code example

<html>

<head>
    <title>Turnstile Example</title> <!-- Connect Turnstile script -->
    <script src="https: //challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
</head>

<body>
    <h1>Form example with Turnstile</h1>
    <form id="my-form"> <label for="username">Name:</label> <input type="text" name="username" id="username" required> <!-- Container for Turnstile -->
        <div class="cf-turnstile" data-sitekey="<YOUR_SITEKEY>" data-callback="onTurnstileSuccess"></div> <button type="submit">Submit</button>
    </form>
    <script>
        // Callback that is called after passing the CAPTCHA
        function onTurnstileSuccess(token) {
          console.log("Received Turnstile token:", token);
          // Save token to hidden form field (optional)
          document.getElementById("cf-turnstile-token")?.remove();
          const input = document.createElement("input");
          input.type = "hidden";
          input.name = "cf-turnstile-response";
          input.id = "cf-turnstile-token";
          input.value = token;
          document.getElementById("my-form").appendChild(input);
        }
        // Form submission
        document.getElementById("my-form").addEventListener("submit", async (e) => {
          e.preventDefault();
          const formData = new FormData(e.target);
          const response = await fetch("/submit-form", {
            method: "POST",
            body: formData
          });
          const result = await response.json();
          if(result.success){
            alert("Form successfully submitted and token verified!");
          } else {
            alert("Turnstile token verification error. Please try again.");
            // Reset widget so the user can complete the CAPTCHA again
            turnstile.reset();
          }
        });
    </script>
</body>

</html>

Step 6 — Configure the server-side part

Server-side verification process:

  • Client: user completes Turnstile on the page → token is created.

  • Form is submitted: token along with form data is sent to the server.

  • Server: makes a POST request to Cloudflare Siteverify API with token and secret.

  • Cloudflare: returns JSON with result (success: true/false) and additional information (action, hostname, completion time).

  • Server: decides whether to allow or reject the user action.

Siteverify API:

POST
https://challenges.cloudflare.com/turnstile/v0/siteverify

Request parameters:

  • secret (required): secret Turnstile key from Cloudflare panel

  • response (required): token received on the client

  • remoteip (optional): user's IP address (recommended)

  • idempotency_key (optional): unique UUID for protection against repeated verifications

Token properties:

  • Maximum length: 2048 characters

  • Valid for 5 minutes

  • Single-use

  • When expired or re-verified, the API will return timeout-or-duplicate error

Verification example in PHP:

<?php
function validateTurnstile($token, $secret, $remoteip = null) {
    $url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
    $data = ['secret' => $secret,
        'response' => $token];
    if ($remoteip) $data['remoteip'] = $remoteip;
    $options = [
        'http' => [
            'header' => "Content-type: application/x-www-form-urlencoded
        ",
            'method' => 'POST',
            'content' => http_build_query($data)
        ]
    ];
    $response = file_get_contents($url, false, stream_context_create($options));
    if ($response === FALSE) {
        return ['success' => false,
            'error-codes' => ['internal-error']];
    }
    return json_decode($response, true);
}
// Usage
$secret_key = 'YOUR_SECRET_KEY';
$token = $_POST['cf-turnstile-response'] ?? '';
$remoteip = $_SERVER['REMOTE_ADDR'];
$result = validateTurnstile($token, $secret_key, $remoteip);
if ($result['success']) {
    echo "Form successfully submitted!";
} else {
    echo "Verification error: " . implode(', ', $result['error-codes']);
}
?>

Note: All detailed instructions for installing and configuring Cloudflare Turnstile, including client and server integration, code examples, error descriptions, and security recommendations, can be found in the official Cloudflare documentation.

Building a Custom CAPTCHA: When and Why

custom captcha is any challenge-response system you build yourself instead of integrating a third-party service. Common lightweight approaches include:

  • Math puzzles — "What is 4 + 9?" rendered as plain text or a simple generated image.

  • Honeypot fields — hidden form fields that only bots tend to fill in.

  • Time-based checks — rejecting submissions completed unrealistically quickly for a human.

  • Custom image challenges — "Click the item that doesn't belong" using your own assets.

Custom captchas can be useful when you need tight branding control, when strict privacy requirements limit third-party scripts, or when your risk profile is low and you only need basic bot friction. However, they typically lack the sophisticated behavioral models of services like GeeTest or Turnstile and can be easier for advanced bots to defeat.

You also take on full responsibility for accessibility (for example, providing audio alternatives) and ongoing maintenance as bot techniques evolve. For critical flows like login, checkout, or password reset, a managed solution is usually safer than a fully custom captcha.

How to Block CAPTCHA Bots Effectively

Implementing CAPTCHA is necessary but rarely sufficient on its own; determined attackers will try to bypass or automate it. To block captcha bots more effectively, combine multiple layers.

Layer 1 — Rate Limiting at the Infrastructure Level

Rate limiting at your web server or CDN makes it harder for bots to brute-force forms even if they can solve or bypass captchas. For example, in Nginx:

# Nginx: limit login endpoint to 10 req/s per IP
limit_req_zone $binary_remote_addr zone=login:10m rate=10r/s;
location /login {
    limit_req zone=login burst=5 nodelay;
    # proxy_pass ...
}

Layer 2 — Honeypot Fields

A hidden field that real users never see but unsophisticated bots may fill is a low-friction defense.

<div aria-hidden="true">
  <input type="text" name="website" tabindex="-1" autocomplete="off">
</div>

On the server, reject any submission where website is non-empty. This adds almost no UX cost while filtering a portion of automated submissions.

Layer 3 — Additional Defense Layers

  • IP reputation filtering: Blocking known datacenter ranges or anonymizer services at the edge can significantly reduce abusive traffic.

  • Token replay prevention: Turnstile tokens must be validated server-side and treated as single-use; discarding reused tokens helps prevent replay attacks.

  • Device or session fingerprinting: External tools can help correlate suspicious patterns across IPs and sessions, though they introduce privacy considerations.

  • Monitoring and alerting: Track verification failure rates and sudden spikes in traffic from particular regions or ASNs as signals of an ongoing attack.

robots
Get started now and automate your solution reCAPTCHA v2

Testing Your CAPTCHA Integration with CapMonster Cloud

Once your CAPTCHA is live, you need a reliable way to exercise the entire request-response cycle in automated test pipelines — without manually solving puzzles on every CI run. CapMonster Cloud is an API-based CAPTCHA solver that supports both GeeTest v3 and Cloudflare Turnstile, among many other types. You submit a task describing the captcha you need solved, and the API returns the solution tokens that your test can inject directly into a form submission.

The full flow for both CAPTCHA types consists of the same three steps: create a task → poll for the result → use the solution.

GeeTest V3 — Full CapMonster Cloud Flow

Step 1 — Create the task

Call createTask immediately after fetching a fresh challenge from your GeeTest registration endpoint. The challenge value is short-lived and single-use.

 

Request:

POST https://api.capmonster.cloud/createTask

{
  "clientKey": "YOUR_CAPMONSTER_API_KEY",
  "task": {
    "type": "GeeTestTask",
    "websiteURL": "https://example.com/your-page",
    "gt": "022397c99c9f646f6477822485f30404",
    "challenge": "7f044f48bc951ecfbfc03842b5e1fe59",
    "geetestApiServerSubdomain": "api-na.geetest.com"
  }
}

 

Response:

{
  "errorId": 0,
  "taskId": 407533072
}

Step 2 — Poll for the result

Call getTaskResult in a loop, waiting a few seconds between each poll. GeeTest V3 tasks typically resolve in 10–30 seconds depending on system load.

 

Request:

POST https://api.capmonster.cloud/getTaskResult

{
  "clientKey": "YOUR_CAPMONSTER_API_KEY",
  "taskId": 407533072
}

 

Response while solving is in progress:

{
  "errorId":0,
  "status":"processing"
}

 

Response when ready:

{
  "errorId":0,
  "status":"ready",
  "solution":{
    "challenge":"0f759dd1ea6c4wc76cedc2991039ca4f23",
    "validate":"6275e26419211d1f526e674d97110e15",
    "seccode":"510cd9735583edcb158601067195a5eb|jordan"
  }
}

All three values in solution — challengevalidate, and seccode — must be submitted to your backend together, exactly as a real browser would send them.

Step 3 — Submit the solution

Post the three solution values to your form endpoint in the same request body fields your GeeTest integration expects:

const result = await pollForResult(taskId);
await fetch('/submit_form.php', {
  method:  'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    geetest_challenge: result.solution.challenge,
    geetest_validate:  result.solution.validate,
    geetest_seccode:   result.solution.seccode,
  })
});

Cloudflare Turnstile — Full CapMonster Cloud Flow

Step 1 — Create the task

Request:

POST https://api.capmonster.cloud/createTask

{
  "clientKey": "API_KEY",
  "task": {
    "type": "TurnstileTask",
    "websiteURL": "http://tsmanaged.zlsupport.com",
    "websiteKey": "0x4AAAAAAABUYP0XeMJF0xoy"
  }
}

For details on parameters description, please refer to the CapMonster documentation.

Response:

{
  "errorId": 0,
  "taskId": 407533072
}

Step 2 — Poll for the result

Turnstile tasks typically resolve in 5–20 seconds.

Request:

POST https://api.capmonster.cloud/getTaskResult

{
  "clientKey": "YOUR_CAPMONSTER_API_KEY",
  "taskId": 407533072
}

Response while solving is in progress:

{
  "errorId": 0,
  "status": "processing"
}

Response when ready:

{
  "errorId": 0,
  "status": "ready",
  "solution": {
    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
    "token": "0.iGX3xsyFCkbGePM3jP4P4khLo6TrLukt8ZzBvwuQOvbC...f61f3082"
  }
}

solution.token is the value that goes into cf-turnstile-response when posting your form. Use solution.userAgent in your HTTP client or Playwright/Selenium instance to match the environment in which the token was solved — mismatches can cause server-side rejections.

Step 3 — Submit the solution

Inject the solved token before submitting the form — either directly into the hidden input or via Cloudflare's JavaScript callback:

// Option A: direct DOM injection (for Playwright/Selenium tests)
await page.evaluate(token => {
  const field = document.querySelector('[name="cf-turnstile-response"]');
  if (field) field.value = token;
}, result.solution.token);

// Option B: trigger the Turnstile callback directly (for SPA tests)
await page.evaluate(token => {
  if (window.turnstileCallback) window.turnstileCallback(token);
}, result.solution.token);
await page.click('#submit-btn');

CapMonster Cloud Error Handling

Both createTask and getTaskResult use the same error envelope. An errorId of 0 always means success; errorId: 1 signals a problem, with the specific error type returned in the string field errorCode. For a full list of possible errors, please refer to the CapMonster documentation.

Example error response:

{
  "errorId": 1,
  "errorCode": "ERROR_KEY_DOES_NOT_EXIST"
}

Best Practices and Tips

Handle Token Expiry Gracefully

GeeTest challenges and Turnstile tokens are time-limited, so you should expect occasional expirations if users wait too long before submitting a form. Implement callbacks like Turnstile's expired-callback or re-fetch a fresh GeeTest challenge and reset the widget instead of failing silently or throwing generic errors.

Accessibility (WCAG 2.1)

  • Cloudflare Turnstile provides visual and alternative challenge flows intended to work with assistive technologies, but you should test them with the screen readers and browsers your users rely on.

  • GeeTest v3's slide puzzle is mouse/touch-centric, which can pose challenges for users with certain motor or coordination impairments; consider an alternate verification path where strong accessibility is required.

  • Avoid making CAPTCHA the only protection mechanism in critical flows for users with disabilities; combine it with other defenses like rate limiting and anomaly detection.

Privacy and Third-Party Data

Using Turnstile or GeeTest involves sending request and interaction data to a third-party provider for analysis and verification. Your privacy policy and consent mechanisms should reflect this, especially under frameworks like GDPR or CCPA where such processing can be considered personal data handling.

Apply CAPTCHA Strategically — Not Everywhere

Form / Endpoint

Recommendation

Login

Apply CAPTCHA, especially after several failed attempts.

Registration

Apply CAPTCHA to reduce fake account creation.

Password reset

Apply, as these flows are sensitive to abuse.

Contact / comment forms

Apply, particularly on public-facing endpoints.

Search

Consider rate limiting first and add CAPTCHA only after repeated abuse.

Public product pages

Typically not needed unless scraping is a major concern.

Separate Test and Production Keys

Cloudflare and most CAPTCHA providers recommend using separate keys or environments for development, staging, and production. Keep your Secret Key and any equivalent private keys in environment variables or a dedicated secrets manager rather than hardcoding them.

Keep SDKs and Integrations Updated

CAPTCHA services update their detection logic and APIs over time in response to new attack techniques. Periodically reviewing provider changelogs or documentation and updating your integrations helps you avoid deprecated endpoints or weakened configurations.

Conclusion

Knowing how to implement CAPTCHA correctly is about more than just dropping a widget into your HTML; it's about pairing a robust client flow with reliable server-side verification and wrapping both in a broader abuse-prevention strategy. GeeTest v3 offers a behavior-based puzzle that can raise the bar for bot operators, while Cloudflare Turnstile fits naturally into Cloudflare-powered sites and emphasizes low-friction verification.

Whichever option you choose, combining CAPTCHA with rate limiting, honeypot fields, and IP reputation filtering significantly increases the effort required to abuse your forms. And when you need to test your integration or automate complex workflows, CapMonster Cloud gives you a programmable way to solve GeeTest and Cloudflare Turnstile challenges through a simple API — making it easier to keep your defenses working as your application evolves.

Ready to add automated CAPTCHA testing to your toolkit? Explore CapMonster Cloud and start integrating their GeeTest and Turnstile task types into your development and QA workflows today.

robots
Get started now and automate your solution reCAPTCHA v2

NB: Please note that the product is intended for automating tests on your own websites and sites you have legal access to.
ItGuy
geear
Affiliate program for software developers
Earn up to 30% from your users’ spending on captcha bypass
✅ Request sent
Thank you for your interest in our partnership program! We will contact you within 7 working days.
Request to Join
Fill out the form to submit an application for the affiliate program.