How to Implement Cloudflare Turnstile Captcha in PHP

How to Implement Cloudflare Turnstile Captcha in PHP

14 Jan. 2026

Why Cloudflare Turnstile?

When I started implementing bot protection for my latest PHP project, I hit a wall. Most CAPTCHA solutions either frustrate users with impossible-to-solve puzzles or come with complex documentation that assumes you're working within a specific framework. That's when I discovered Cloudflare Turnstile and decided to create a straightforward implementation guide.

Cloudflare Turnstile is a user-friendly CAPTCHA alternative that doesn't require users to solve puzzles, identify traffic lights, or decipher distorted text. It works silently in the background, validating that your visitors are real humans without creating friction in the user experience. Best of all, you don't need to route your traffic through Cloudflare to use it.

The Problem I Encountered

While Cloudflare's documentation is comprehensive, I struggled to find simple, framework-agnostic examples showing how to validate Turnstile responses server-side in pure PHP. Most tutorials either focused on JavaScript implementation or required specific PHP frameworks. I needed something basic that I could adapt to any PHP project.

That's why I created this repository with working examples that you can copy, paste, and customize for your needs.

Getting Started: Prerequisites

Before diving into the code, you'll need to set up Cloudflare Turnstile and obtain your API keys:

  1. Go to the Cloudflare Dashboard
  2. Navigate to Turnstile in your account
  3. Create a new site and register your domain/hostname
  4. You'll receive two keys: 
    • Site Key: Used in your frontend HTML
    • Secret Key: Used for server-side verification (keep this secure!)

Implementation: The Frontend

First, let's create the HTML form with the Turnstile widget. This is surprisingly simple:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cloudflare Turnstile Example</title>
    <!-- Load Cloudflare Turnstile Script -->
    <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
</head>
<body>
    <h1>Contact Form</h1>
    
    <form action="submit-example.php" method="POST">
        <label for="name">Name:</label>
        <input type="text" id="name" name="name" required>
        
        <label for="email">Email:</label>
        <input type="email" id="email" name="email" required>
        
        <label for="message">Message:</label>
        <textarea id="message" name="message" required></textarea>
        
        <!-- Cloudflare Turnstile Widget -->
        <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY_HERE"></div>
        
        <button type="submit">Submit</button>
    </form>
</body>
</html>

Key points:

  • Include the Turnstile script in your <head> section
  • Add the cf-turnstile div where you want the widget to appear
  • Replace YOUR_SITE_KEY_HERE with your actual Site Key

The widget will automatically render and handle the challenge. When the form is submitted, Turnstile adds a hidden field called cf-turnstile-response containing a token.

Server-Side Validation: The Important Part

Here's where most guides leave you hanging. The server-side validation is crucial because it prevents malicious users from bypassing the CAPTCHA by simply submitting the form without going through Turnstile.

<?php
// submit-example.php

// Check if form was submitted
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    
    // Get the Turnstile response token
    $turnstileResponse = $_POST['cf-turnstile-response'] ?? '';
    
    // Your Secret Key from Cloudflare
    $secretKey = 'YOUR_SECRET_KEY_HERE';
    
    // Cloudflare Turnstile verification endpoint
    $verifyUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
    
    // Prepare the data for verification
    $data = [
        'secret' => $secretKey,
        'response' => $turnstileResponse,
        'remoteip' => $_SERVER['REMOTE_ADDR'] // Optional but recommended
    ];
    
    // Initialize cURL
    $curl = curl_init();
    
    curl_setopt_array($curl, [
        CURLOPT_URL => $verifyUrl,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query($data),
        CURLOPT_RETURNTRANSFER => true
    ]);
    
    // Execute the request
    $response = curl_exec($curl);
    $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    curl_close($curl);
    
    // Parse the JSON response
    $result = json_decode($response, true);
    
    // Check if verification was successful
    if ($httpCode === 200 && isset($result['success']) && $result['success'] === true) {
        // CAPTCHA verification passed!
        
        // Process your form data here
        $name = htmlspecialchars($_POST['name']);
        $email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
        $message = htmlspecialchars($_POST['message']);
        
        // Your form processing logic
        echo "Thank you, " . $name . "! Your message has been received.";
        
        // Send email, save to database, etc.
        
    } else {
        // CAPTCHA verification failed
        
        $errorCodes = $result['error-codes'] ?? ['unknown-error'];
        echo "CAPTCHA verification failed. Error codes: " . implode(', ', $errorCodes);
        
        // Log the error or redirect back to form
    }
    
} else {
    // Handle non-POST requests
    header('Location: form-example.php');
    exit;
}
?>

Understanding the Validation Flow

  1. Token Retrieval: When a user submits the form, Turnstile automatically includes a response token in the cf-turnstile-response POST field
  2. Server Request: Your PHP script sends this token, along with your Secret Key, to Cloudflare's verification endpoint
  3. Verification: Cloudflare validates the token and returns a JSON response indicating success or failure
  4. Processing: Only if verification succeeds should you process the form data

Common Error Codes

Turnstile may return error codes in the response. Here are the most common ones:

  • missing-input-secret: The secret key is missing
  • invalid-input-secret: The secret key is invalid
  • missing-input-response: The response token is missing
  • invalid-input-response: The response token is invalid or has expired
  • timeout-or-duplicate: The response token has already been validated or has timed out

Security Best Practices

  1. Never expose your Secret Key: Keep it in environment variables or a secure configuration file
  2. Always validate server-side: Client-side validation alone can be bypassed
  3. Include the remote IP: This helps Cloudflare detect suspicious patterns
  4. Handle errors gracefully: Don't reveal too much information in error messages
  5. Use HTTPS: Always use secure connections when transmitting sensitive data

Customizing the Widget

Turnstile supports several customization options:

<div class="cf-turnstile" 
     data-sitekey="YOUR_SITE_KEY_HERE"
     data-theme="light"
     data-size="normal"
     data-language="en">
</div>

Available options:

  • data-theme: "light", "dark", or "auto"
  • data-size: "normal" or "compact"
  • data-language: Any supported language code
  • data-appearance: "always" or "interaction-only"

Why This Approach Works

This implementation is intentionally simple and framework-agnostic. It uses only:

  • Standard PHP (no frameworks required)
  • cURL for HTTP requests (available in most PHP installations)
  • Basic error handling

You can easily integrate this into:

  • Custom PHP applications
  • WordPress sites (with minor modifications)
  • Legacy codebases
  • Any PHP framework (CodeIgniter, Laravel, Symfony, etc.)

Testing Your Implementation

Cloudflare provides test keys that you can use during development:

  • Site Key: 1x00000000000000000000AA (always passes)
  • Secret Key: 1x0000000000000000000000000000000AA (always passes)

Use these keys to test your implementation before deploying with real keys.

Conclusion

Implementing Cloudflare Turnstile in PHP doesn't have to be complicated. With this straightforward approach, you can add robust bot protection to your forms without frustrating your users or getting lost in complex documentation.

The complete working code is available in my GitHub repository, where you can find both the form and submission examples ready to use.

If you encounter any issues or have questions, feel free to open an issue on GitHub. I'll do my best to help!

Related Resources:

More articles: