Tested and maintained, so you don't have to. $99/year.
Turnstile
Cloudflare Turnstile integration with configurable timeout and multiple HTTP transport methods.
Why Use This Service?
What you'd build yourself: Cloudflare API integration, widget rendering, server-side token verification, and proper error handling.
Maintained for you: tracks Cloudflare's API changes so your integration doesn't break.
What Pro provides:
- Privacy-focused alternative to reCAPTCHA (no cookies required)
- Simple
scripts()andpassed()API - Automatic widget rendering
- Works with
file_get_contentsor cURL
Installation
Flick Pro requires a license. See Pro installation for setup instructions.
Configuration
Get your Turnstile keys from the Cloudflare Dashboard.
$config = [ 'services' => [ 'turnstile' => [ 'siteKey' => 'your-site-key', 'secretKey' => 'your-secret-key' ] ] ]; $form = new Flick($config);
Full Configuration
$config = [ 'services' => [ 'turnstile' => [ 'siteKey' => 'your-site-key', 'secretKey' => 'your-secret-key', 'timeout' => 10, // API request timeout in seconds 'curl' => false, // Use cURL instead of file_get_contents 'expectedAction' => 'submit', // Reject tokens minted for a different action 'expectedHostname' => 'example.com' // Reject tokens minted on another host ] ] ];
expectedAction and expectedHostname are both optional and both off unless you set
them. Turning them on stops a token issued elsewhere on your site — or on someone
else's copy of your page — from being replayed against this form.
expectedAction must be 1–32 characters of letters, numbers, underscores or hyphens;
anything else is rejected when the form is built, rather than silently failing every
submission. Flick renders it onto the widget for you, so the action Cloudflare mints
the token for is always the one Flick checks it against — you do not add
data-action yourself.
Basic Usage
1. Add Scripts to Your Page
Include the Turnstile widget in your HTML:
<?php require 'vendor/autoload.php'; $form = new Flick\Flick([ 'services' => [ 'turnstile' => [ 'siteKey' => 'your-site-key', 'secretKey' => 'your-secret-key' ] ] ]); ?> <!DOCTYPE html> <html> <head> <title>Contact Form</title> </head> <body> <?php $form->open(); ?> <?php $form->text('name', 'Name'); ?> <?php $form->text('email', 'Email'); ?> <?php $form->textarea('message', 'Message'); ?> <!-- The widget must be INSIDE the form, before the submit button — Turnstile only submits its response token from inside the <form> --> <?= $form->turnstile->scripts() ?> <?php $form->submit(); ?> <?php $form->close(); ?> </body> </html>
Build the form with open()/close() and the individual field methods (rather
than the one-shot create()) so the widget can be placed inside the <form>
element. create() renders a complete, already-closed form — a widget printed
after it sits outside the form, its token is never submitted, and passed()
always fails with "Missing Turnstile response token".
2. Validate on Submission
Check if the Turnstile validation passed:
if ($form->submitted()) { if (!$form->turnstile->passed()) { $form->addError('turnstile', 'Security check failed. Please try again.'); } if ($form->ok()) { $request = $form->request('Name[required], Email[required, email], Message[required]'); if ($form->ok()) { // Process form... } } }
Methods
scripts()
Generate the Turnstile JavaScript and widget HTML for form integration.
$form->turnstile->scripts(): string
Returns: HTML script tag and widget div that:
- Load the Cloudflare Turnstile library
- Render the widget in your form
Tip
Place the widget inside the <form> element, typically before the submit
button. Cloudflare only submits the response token when the widget sits inside
the form it belongs to.
passed()
Validate the Turnstile response from form submission.
$form->turnstile->passed(): bool
Returns: true if validation passed, false otherwise.
Validation checks:
- Request method is POST
- Turnstile response token is present
- Cloudflare API verification succeeds
- Hostname matches
expectedHostname(if configured) - Action matches
expectedAction(if configured)
When any check fails, passed() also records the reason in Flick's error bag under
the key turnstile — for example Missing Turnstile response token. Adding your own
addError('turnstile', ...) replaces that message with yours.
Complete Example
<?php require 'vendor/autoload.php'; $form = new Flick\Flick([ 'services' => [ 'turnstile' => [ 'siteKey' => $_ENV['TURNSTILE_SITE_KEY'], 'secretKey' => $_ENV['TURNSTILE_SECRET_KEY'] ], 'mail' => [ 'fromAddress' => 'noreply@example.com', 'mailer' => [ 'transport' => 'smtp', 'host' => 'smtp.example.com', 'port' => 587, 'username' => 'user', 'password' => 'pass' ] ] ] ]); // Handle form submission if ($form->submitted()) { // Validate Turnstile first if (!$form->turnstile->passed()) { $form->addError('form', 'Security check failed. Please try again.'); } if ($form->ok()) { $request = $form->request(' Name[required, min:2], Email[required, email], Message[required, min:10] '); if ($form->ok()) { // Send email $form->mail->sendFormData( 'admin@example.com', 'New Contact Form Submission', $request ); $form->successMessage('Thank you! Your message has been sent.'); } } } ?> <!DOCTYPE html> <html> <head> <title>Contact Us</title> </head> <body> <h1>Contact Us</h1> <?php if ($form->errorsIsNotEmpty()): ?> <div class="errors"> <ul> <?php foreach ($form->getErrors() as $error): ?> <li><?= htmlspecialchars($error) ?></li> <?php endforeach; ?> </ul> </div> <?php endif; ?> <?php $form->open(); ?> <?php $form->text('name', 'Name'); ?> <?php $form->text('email', 'Email'); ?> <?php $form->textarea('message', 'Message'); ?> <!-- Inside the form, so the response token is submitted with it --> <?= $form->turnstile->scripts() ?> <?php $form->submit(); ?> <?php $form->close(); ?> </body> </html>
How It Works
Client Side
When scripts() is called, it generates:
- A script tag that loads Cloudflare's Turnstile library
- A div with the
cf-turnstileclass and your site key
The widget automatically:
- Renders the challenge (if needed)
- Adds a hidden
cf-turnstile-responsefield to the form it sits inside (which is why the widget must be placed inside your<form>element)
Server Side
When passed() is called, it:
- Checks the request is a POST request
- Extracts the
cf-turnstile-responsetoken - Sends the token to Cloudflare's verification API
- Returns true if verification succeeds
Turnstile vs reCAPTCHA
| Feature | Turnstile | reCAPTCHA v3 |
|---|---|---|
| Visible widget | Yes (non-intrusive) | No (invisible) |
| Score-based | No | Yes |
| Privacy | No cookies required | Uses cookies |
| Free tier | Unlimited | 1M/month |
Choose Turnstile for a privacy-focused, simpler integration. Choose reCAPTCHA for score-based bot detection.
Using cURL
By default, Flick uses file_get_contents for API requests. To use cURL instead:
'turnstile' => [ 'siteKey' => 'your-site-key', 'secretKey' => 'your-secret-key', 'curl' => true ]
Tip
Use cURL if allow_url_fopen is disabled in your PHP configuration.
Security Features
- Server-Side Verification: Tokens are verified with Cloudflare's API
- IP Validation: Client IP is sent for additional verification
- Secure API Communication: SSL verification enabled for API requests
- Configurable Timeouts: Prevents hanging requests