---
title: Validate the token
description: Validate Turnstile tokens on your server with the siteverify API.
image: https://developers.cloudflare.com/core-services-preview.png
---

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/turnstile/llms.txt  
> Use this file to discover all available pages before exploring further.

[Skip to content](#%5Ftop) 

### Tags

[ REST API ](https://developers.cloudflare.com/search/?tags=REST%20API) 

# Validate the token

Learn how to securely validate Turnstile tokens on your server using the Siteverify API.

Mandatory server-side validation

You must call the Siteverify API to complete your Turnstile implementation. The client-side widget alone does not protect your forms.

Server-side validation is required because:

* **Tokens can be forged.** An attacker can submit any string to your form endpoint without completing a challenge.
* **Tokens expire.** Each token is valid for 300 seconds (5 minutes) after generation.
* **Tokens are single-use.** Each token can only be validated once. A replayed token will be rejected with the `timeout-or-duplicate` error code.

## Process

1. Client generates token: Visitor completes Turnstile challenge on your webpage.
2. Token sent to server: Form submission includes the Turnstile token.
3. Server validates token: Your server calls Cloudflare's Siteverify API.
4. Cloudflare responds: Returns `success` or `failure` and additional data.
5. Server takes action: Allow or reject the original request based on validation.

## Siteverify API overview

Endpoint

```

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


```

### Request format

The API accepts both `application/x-www-form-urlencoded` and `application/json` requests, but always returns JSON responses.

#### Required parameters

| Parameter        | Required | Description                                             |
| ---------------- | -------- | ------------------------------------------------------- |
| secret           | Yes      | Your widget's secret key from the Cloudflare dashboard  |
| response         | Yes      | The token from the client-side widget                   |
| remoteip         | No       | The visitor's IP address                                |
| idempotency\_key | No       | A UUID you generate to safely retry validation requests |

#### Token characteristics

* Maximum length: 2048 characters
* Validity period: 300 seconds (5 minutes) from generation
* Single use: Each token can only be validated once
* Automatic expiry: Tokens automatically expire and cannot be reused

The validation token issued by Turnstile is valid for five minutes. If a user submits the form after this period, the token is considered expired. In this scenario, the server-side verification API will return a failure, and the `error-codes` field in the response will include `timeout-or-duplicate`.

To ensure a successful validation, the visitor must initiate the request and submit the token to your backend within the five-minute window. Otherwise, the Turnstile widget needs to be refreshed to generate a new token. This can be done using the `turnstile.reset` function.

---

## Basic validation examples

* [  JavaScript ](#tab-panel-8659)
* [  PHP ](#tab-panel-8660)
* [  Python ](#tab-panel-8661)
* [  Java ](#tab-panel-8662)
* [  C# ](#tab-panel-8663)

#### JSON

JavaScript

```

const SECRET_KEY = "your-secret-key";


async function validateTurnstile(token, remoteip) {

  try {

    const response = await fetch(

      "https://challenges.cloudflare.com/turnstile/v0/siteverify",

      {

        method: "POST",

        headers: {

          "Content-Type": "application/json",

        },

        body: JSON.stringify({

          secret: SECRET_KEY,

          response: token,

          remoteip: remoteip,

        }),

      },

    );


    const result = await response.json();

    return result;

  } catch (error) {

    console.error("Turnstile validation error:", error);

    return { success: false, "error-codes": ["internal-error"] };

  }

}


```

#### Form Data

JavaScript

```

const SECRET_KEY = "your-secret-key";


async function validateTurnstile(token, remoteip) {

  const formData = new FormData();

  formData.append("secret", SECRET_KEY);

  formData.append("response", token);

  formData.append("remoteip", remoteip);


  try {

    const response = await fetch(

      "https://challenges.cloudflare.com/turnstile/v0/siteverify",

      {

        method: "POST",

        body: formData,

      },

    );


    const result = await response.json();

    return result;

  } catch (error) {

    console.error("Turnstile validation error:", error);

    return { success: false, "error-codes": ["internal-error"] };

  }

}


// Usage in form handler

async function handleFormSubmission(request) {

  const body = await request.formData();

  const token = body.get("cf-turnstile-response");

  const ip =

    request.headers.get("CF-Connecting-IP") ||

    request.headers.get("X-Forwarded-For") ||

    "unknown";


  const validation = await validateTurnstile(token, ip);


  if (validation.success) {

    // Token is valid - process the form

    console.log("Valid submission from:", validation.hostname);

    return processForm(body);

  } else {

    // Token is invalid - reject the submission

    console.log("Invalid token:", validation["error-codes"]);

    return new Response("Invalid verification", { status: 400 });

  }

}


```

```

<?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\r\n",

            'method' => 'POST',

            'content' => http_build_query($data)

        ]

    ];


    $context = stream_context_create($options);

    $response = file_get_contents($url, false, $context);


    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['HTTP_CF_CONNECTING_IP'] ??

$\_SERVER['HTTP_X_FORWARDED_FOR'] ??

$\_SERVER['REMOTE_ADDR'];


$validation = validateTurnstile($token, $secret_key, $remoteip);


if ($validation['success']) {

// Valid token - process form

echo "Form submission successful!";

// Process your form data here

} else {

// Invalid token - show error

echo "Verification failed. Please try again.";

error_log('Turnstile validation failed: ' . implode(', ', $validation['error-codes']));

}

?>


```

Python

```

import requests


def validate_turnstile(token, secret, remoteip=None):

    url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'


    data = {

        'secret': secret,

        'response': token

    }


    if remoteip:

        data['remoteip'] = remoteip


    try:

        response = requests.post(url, data=data, timeout=10)

        response.raise_for_status()

        return response.json()

    except requests.RequestException as e:

        print(f"Turnstile validation error: {e}")

        return {'success': False, 'error-codes': ['internal-error']}


# Usage with Flask

from flask import Flask, request, jsonify


app = Flask(__name__)

SECRET_KEY = 'your-secret-key'


@app.route('/submit-form', methods=['POST'])

def submit_form():

    token = request.form.get('cf-turnstile-response')

    remoteip = request.headers.get('CF-Connecting-IP') or \

               request.headers.get('X-Forwarded-For') or \

               request.remote_addr


    validation = validate_turnstile(token, SECRET_KEY, remoteip)


    if validation['success']:

        # Valid token - process form

        return jsonify({'status': 'success', 'message': 'Form submitted successfully'})

    else:

        # Invalid token - reject submission

        return jsonify({

            'status': 'error',

            'message': 'Verification failed',

            'errors': validation['error-codes']

        }), 400


```

```

import org.springframework.web.client.RestTemplate;

import org.springframework.util.LinkedMultiValueMap;

import org.springframework.util.MultiValueMap;

import org.springframework.http.HttpEntity;

import org.springframework.http.HttpHeaders;

import org.springframework.http.MediaType;

import org.springframework.http.ResponseEntity;


@Service

public class TurnstileService {

private static final String SITEVERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";

private final String secretKey = "your-secret-key";

private final RestTemplate restTemplate = new RestTemplate();


    public TurnstileResponse validateToken(String token, String remoteip) {

        HttpHeaders headers = new HttpHeaders();

        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);


        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();

        params.add("secret", secretKey);

        params.add("response", token);

        if (remoteip != null) {

            params.add("remoteip", remoteip);

        }


        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);


        try {

            ResponseEntity<TurnstileResponse> response = restTemplate.postForEntity(

                SITEVERIFY_URL, request, TurnstileResponse.class);

            return response.getBody();

        } catch (Exception e) {

            TurnstileResponse errorResponse = new TurnstileResponse();

            errorResponse.setSuccess(false);

            errorResponse.setErrorCodes(List.of("internal-error"));

            return errorResponse;

        }

    }


}


// Controller usage

@PostMapping("/submit-form")

public ResponseEntity<?> submitForm(

@RequestParam("cf-turnstile-response") String token,

HttpServletRequest request) {


    String remoteip = request.getHeader("CF-Connecting-IP");

    if (remoteip == null) {

        remoteip = request.getHeader("X-Forwarded-For");

    }

    if (remoteip == null) {

        remoteip = request.getRemoteAddr();

    }


    TurnstileResponse validation = turnstileService.validateToken(token, remoteip);


    if (validation.isSuccess()) {

        // Valid token - process form

        return ResponseEntity.ok("Form submitted successfully");

    } else {

        // Invalid token - reject submission

        return ResponseEntity.badRequest()

            .body("Verification failed: " + validation.getErrorCodes());

    }


}


```

```

using System.Text.Json;


public class TurnstileService

{

    private readonly HttpClient _httpClient;

    private readonly string _secretKey = "your-secret-key";

    private const string SiteverifyUrl = "https://challenges.cloudflare.com/turnstile/v0/siteverify";


    public TurnstileService(HttpClient httpClient)

    {

        _httpClient = httpClient;

    }


    public async Task<TurnstileResponse> ValidateTokenAsync(string token, string remoteip = null)

    {

        var parameters = new Dictionary<string, string>

        {

            { "secret", _secretKey },

            { "response", token }

        };


        if (!string.IsNullOrEmpty(remoteip))

        {

            parameters.Add("remoteip", remoteip);

        }


        var postContent = new FormUrlEncodedContent(parameters);


        try

        {

            var response = await _httpClient.PostAsync(SiteverifyUrl, postContent);

            var stringContent = await response.Content.ReadAsStringAsync();


            return JsonSerializer.Deserialize<TurnstileResponse>(stringContent);

        }

        catch (Exception ex)

        {

            return new TurnstileResponse

            {

                Success = false,

                ErrorCodes = new[] { "internal-error" }

            };

        }

    }

}


// Controller usage

[HttpPost("submit-form")]

public async Task<IActionResult> SubmitForm([FromForm] string cfTurnstileResponse)

{

    var remoteip = HttpContext.Request.Headers["CF-Connecting-IP"].FirstOrDefault() ??

                   HttpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault() ??

                   HttpContext.Connection.RemoteIpAddress?.ToString();


    var validation = await _turnstileService.ValidateTokenAsync(cfTurnstileResponse, remoteip);


    if (validation.Success)

    {

        // Valid token - process form

        return Ok("Form submitted successfully");

    }

    else

    {

        // Invalid token - reject submission

        return BadRequest($"Verification failed: {string.Join(", ", validation.ErrorCodes)}");

    }

}


```

---

## Advanced validation techniques

Idempotency keys for retry operation

```

const crypto = require("crypto");


async function validateWithRetry(token, remoteip, maxRetries = 3) {

  const idempotencyKey = crypto.randomUUID();


  for (let attempt = 1; attempt <= maxRetries; attempt++) {

    try {

      const formData = new FormData();

      formData.append("secret", SECRET_KEY);

      formData.append("response", token);

      formData.append("remoteip", remoteip);

      formData.append("idempotency_key", idempotencyKey);


      const response = await fetch(

        "https://challenges.cloudflare.com/turnstile/v0/siteverify",

        {

          method: "POST",

          body: formData,

        },

      );


      const result = await response.json();


      if (response.ok) {

        return result;

      }


      // If this is the last attempt, return the error

      if (attempt === maxRetries) {

        return result;

      }


      // Wait before retrying (exponential backoff)

      await new Promise((resolve) =>

        setTimeout(resolve, Math.pow(2, attempt) * 1000),

      );

    } catch (error) {

      if (attempt === maxRetries) {

        return { success: false, "error-codes": ["internal-error"] };

      }

    }

  }

}


```

Enhanced validation with custom checks

```

async function validateTurnstileEnhanced(

  token,

  remoteip,

  expectedAction = null,

  expectedHostname = null,

) {

  const validation = await validateTurnstile(token, remoteip);


  if (!validation.success) {

    return {

      valid: false,

      reason: "turnstile_failed",

      errors: validation["error-codes"],

    };

  }


  // Check if action matches expected value (if specified)

  if (expectedAction && validation.action !== expectedAction) {

    return {

      valid: false,

      reason: "action_mismatch",

      expected: expectedAction,

      received: validation.action,

    };

  }


  // Check if hostname matches expected value (if specified)

  if (expectedHostname && validation.hostname !== expectedHostname) {

    return {

      valid: false,

      reason: "hostname_mismatch",

      expected: expectedHostname,

      received: validation.hostname,

    };

  }


  // Check token age (warn if older than 4 minutes)

  const challengeTime = new Date(validation.challenge_ts);

  const now = new Date();

  const ageMinutes = (now - challengeTime) / (1000 * 60);


  if (ageMinutes > 4) {

    console.warn(`Token is ${ageMinutes.toFixed(1)} minutes old`);

  }


  return {

    valid: true,

    data: validation,

    tokenAge: ageMinutes,

  };

}


// Usage

const result = await validateTurnstileEnhanced(

  token,

  remoteip,

  "login", // expected action

  "example.com", // expected hostname

);


if (result.valid) {

  // Process the request

  console.log("Validation successful:", result.data);

} else {

  // Handle validation failure

  console.log("Validation failed:", result.reason);

}


```

---

## API response format

* [ Successful response ](#tab-panel-8664)
* [ Failed response ](#tab-panel-8665)

Example

```

{

  "success": true,

  "challenge_ts": "2022-02-28T15:14:30.096Z",

  "hostname": "example.com",

  "error-codes": [],

  "action": "login",

  "cdata": "sessionid-123456789",

  "metadata": {

    "ephemeral_id": "x:9f78e0ed210960d7693b167e"

  }

}


```

Example

```

{

  "success": false,

  "error-codes": ["invalid-input-response"]

}


```

### Response fields

| Field                  | Description                                      |
| ---------------------- | ------------------------------------------------ |
| success                | Boolean indicating if validation was successful  |
| challenge\_ts          | ISO 8601 timestamp when the challenge was solved |
| hostname               | Hostname where the challenge was served          |
| error-codes            | Array of error codes (if validation failed)      |
| action                 | Custom action identifier from client-side        |
| cdata                  | Custom data payload from client-side             |
| metadata.ephemeral\_id | Device fingerprint ID (Enterprise only)          |

### Error codes reference

| Error code             | Description                             | Action required                                   |
| ---------------------- | --------------------------------------- | ------------------------------------------------- |
| missing-input-secret   | Secret parameter not provided           | Ensure secret key is included                     |
| invalid-input-secret   | Secret key is invalid or expired        | Check your secret key in the Cloudflare dashboard |
| missing-input-response | Response parameter was not provided     | Ensure token is included                          |
| invalid-input-response | Token is invalid, malformed, or expired | User should retry the challenge                   |
| bad-request            | Request is malformed                    | Check request format and parameters               |
| timeout-or-duplicate   | Token has already been validated        | Each token can only be used once                  |
| internal-error         | Internal error occurred                 | Retry the request                                 |

---

## Implementation

Example implementation

```

class TurnstileValidator {

  constructor(secretKey, timeout = 10000) {

    this.secretKey = secretKey;

    this.timeout = timeout;

  }


  async validate(token, remoteip, options = {}) {

    // Input validation

    if (!token || typeof token !== "string") {

      return { success: false, error: "Invalid token format" };

    }


    if (token.length > 2048) {

      return { success: false, error: "Token too long" };

    }


    // Prepare request

    const controller = new AbortController();

    const timeoutId = setTimeout(() => controller.abort(), this.timeout);


    try {

      const formData = new FormData();

      formData.append("secret", this.secretKey);

      formData.append("response", token);


      if (remoteip) {

        formData.append("remoteip", remoteip);

      }


      if (options.idempotencyKey) {

        formData.append("idempotency_key", options.idempotencyKey);

      }


      const response = await fetch(

        "https://challenges.cloudflare.com/turnstile/v0/siteverify",

        {

          method: "POST",

          body: formData,

          signal: controller.signal,

        },

      );


      const result = await response.json();


      // Additional validation

      if (result.success) {

        if (

          options.expectedAction &&

          result.action !== options.expectedAction

        ) {

          return {

            success: false,

            error: "Action mismatch",

            expected: options.expectedAction,

            received: result.action,

          };

        }


        if (

          options.expectedHostname &&

          result.hostname !== options.expectedHostname

        ) {

          return {

            success: false,

            error: "Hostname mismatch",

            expected: options.expectedHostname,

            received: result.hostname,

          };

        }

      }


      return result;

    } catch (error) {

      if (error.name === "AbortError") {

        return { success: false, error: "Validation timeout" };

      }


      console.error("Turnstile validation error:", error);

      return { success: false, error: "Internal error" };

    } finally {

      clearTimeout(timeoutId);

    }

  }

}


// Usage

const validator = new TurnstileValidator(process.env.TURNSTILE_SECRET_KEY);


const result = await validator.validate(token, remoteip, {

  expectedAction: "login",

  expectedHostname: "example.com",

});


if (result.success) {

  // Process the request

} else {

  // Handle failure

  console.log("Validation failed:", result.error);

}


```

---

## Testing

You can test the dummy token generated with testing sitekey via Siteverify API with the testing secret key. Your production secret keys will reject dummy tokens.

Refer to [Testing](https://developers.cloudflare.com/turnstile/troubleshooting/testing/) for more information.

---

## Best practices

### Security

* Store your secret keys securely. Use environment variables or secure key management.
* Validate the token on every request. Never trust client-side validation alone.
* Check additional fields. Validate the action and hostname when specified.
* Monitor for abuse and log failed validations and unusual patterns.
* Use HTTPS. Always validate over secure connections.
* Only call the Siteverify API in your backend environment. If you expose the secret key in the front-end client code to call Siteverify, attackers can bypass the security check. Ensure that your client-side code sends the validation token to your backend, and that your backend is the sole caller of the Siteverify API.

### Performance

* Set reasonable timeouts. Do not wait indefinitely for Siteverify responses.
* Implement retry logic and handle temporary network issues.
* Cache validation results for the same token, if it is needed for your flow.
* Monitor your API latency. Track the Siteverify response time.

### Error handling

* Have fallback behavior for API failures.
* Use user-friendly messaging. Do not expose internal error details to users.
* Properly log errors for debugging without exposing secrets.
* Rate limit to protect against validation flooding.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/turnstile/","name":"Turnstile"}},{"@type":"ListItem","position":3,"item":{"@id":"/turnstile/get-started/","name":"Get started"}},{"@type":"ListItem","position":4,"item":{"@id":"/turnstile/get-started/server-side-validation/","name":"Validate the token"}}]}
```
