Table of Contents

Turnstile Widgets

Manage Turnstile widgets for bot protection. Turnstile is Cloudflare's CAPTCHA alternative that provides protection without user friction.

Overview

Access Turnstile through cf.Turnstile:

public class TurnstileService(ICloudflareApiClient cf)
{
    public async Task<TurnstileWidget> CreateWidgetAsync(
        string accountId,
        string name,
        IEnumerable<string> domains)
    {
        return await cf.Turnstile.CreateWidgetAsync(accountId,
            new CreateTurnstileWidgetRequest(
                Name: name,
                Domains: domains.ToList(),
                Mode: WidgetMode.Managed
            ));
    }
}
Warning

Widget secrets are only returned when created or rotated. Store them securely.

Listing Widgets

With Pagination

var result = await cf.Turnstile.ListWidgetsAsync(accountId,
    new ListTurnstileWidgetsFilters(
        Order: TurnstileOrderField.CreatedOn,
        Direction: ListOrderDirection.Desc
    ));

foreach (var widget in result.Result)
{
    Console.WriteLine($"{widget.Name}: {widget.Sitekey}");
    Console.WriteLine($"  Mode: {widget.Mode}");
    Console.WriteLine($"  Domains: {string.Join(", ", widget.Domains)}");
}

List All Widgets

await foreach (var widget in cf.Turnstile.ListAllWidgetsAsync(accountId))
{
    Console.WriteLine($"{widget.Name} [{widget.Mode}]");
}

Getting Widget Details

var widget = await cf.Turnstile.GetWidgetAsync(accountId, sitekey);

Console.WriteLine($"Name: {widget.Name}");
Console.WriteLine($"Sitekey: {widget.Sitekey}");
Console.WriteLine($"Mode: {widget.Mode}");
Console.WriteLine($"Domains: {string.Join(", ", widget.Domains)}");
Console.WriteLine($"Bot Fight Mode: {widget.BotFightMode}");
Console.WriteLine($"Created: {widget.CreatedOn}");
Note

The secret is not returned when getting widget details. It's only available at creation or after rotation.

Creating Widgets

Basic Widget

var widget = await cf.Turnstile.CreateWidgetAsync(accountId,
    new CreateTurnstileWidgetRequest(
        Name: "Login Form",
        Domains: new[] { "example.com", "www.example.com" },
        Mode: WidgetMode.Managed
    ));

// IMPORTANT: Store this secret securely!
Console.WriteLine($"Sitekey: {widget.Sitekey}");
Console.WriteLine($"Secret: {widget.Secret}");

Widget Modes

WidgetMode is an extensible enum controlling user interaction level:

Mode API Value Description
Managed managed Cloudflare dynamically decides when to show challenges
NonInteractive non-interactive Challenge runs without visible interaction
Invisible invisible Completely invisible - automatic verification
using Cloudflare.NET.Turnstile.Models;

// Invisible widget for seamless UX
var invisible = await cf.Turnstile.CreateWidgetAsync(accountId,
    new CreateTurnstileWidgetRequest(
        Name: "Invisible Protection",
        Domains: new[] { "example.com" },
        Mode: WidgetMode.Invisible
    ));

// Extensible for future modes
WidgetMode customMode = "future-mode";

With Bot Fight Mode

Enable additional bot protection:

var widget = await cf.Turnstile.CreateWidgetAsync(accountId,
    new CreateTurnstileWidgetRequest(
        Name: "Contact Form",
        Domains: new[] { "example.com" },
        Mode: WidgetMode.Managed,
        BotFightMode: true
    ));

With Custom Region

var widget = await cf.Turnstile.CreateWidgetAsync(accountId,
    new CreateTurnstileWidgetRequest(
        Name: "EU Form",
        Domains: new[] { "eu.example.com" },
        Mode: WidgetMode.Managed,
        Region: TurnstileRegion.World
    ));

Updating Widgets

var updated = await cf.Turnstile.UpdateWidgetAsync(accountId, sitekey,
    new UpdateTurnstileWidgetRequest(
        Name: "Updated Widget Name",
        Domains: new[] { "example.com", "api.example.com" },
        Mode: WidgetMode.NonInteractive
    ));

Add Domain

var widget = await cf.Turnstile.GetWidgetAsync(accountId, sitekey);

var domains = widget.Domains.ToList();
domains.Add("new.example.com");

await cf.Turnstile.UpdateWidgetAsync(accountId, sitekey,
    new UpdateTurnstileWidgetRequest(
        Name: widget.Name,
        Domains: domains,
        Mode: widget.Mode
    ));

Deleting Widgets

await cf.Turnstile.DeleteWidgetAsync(accountId, sitekey);
Console.WriteLine($"Deleted widget: {sitekey}");

Rotating Secrets

With Grace Period (Default)

The old secret remains valid for 2 hours:

var result = await cf.Turnstile.RotateSecretAsync(accountId, sitekey);

Console.WriteLine($"New secret: {result.Secret}");
Console.WriteLine("Old secret valid for 2 more hours");

Immediate Invalidation

Revoke the old secret immediately:

var result = await cf.Turnstile.RotateSecretAsync(
    accountId,
    sitekey,
    invalidateImmediately: true);

Console.WriteLine($"New secret: {result.Secret}");
Console.WriteLine("Old secret is now invalid");
Warning

Immediate invalidation may cause validation failures for in-flight requests.

Models Reference

TurnstileWidget

Property Type Description
Sitekey string Public key for client-side integration
Secret string? Private key (only on create/rotate)
Name string Widget name
Domains IReadOnlyList<string> Allowed domains
Mode WidgetMode Challenge mode
BotFightMode bool Extra bot protection
Region TurnstileRegion? Processing region
CreatedOn DateTime Creation timestamp
ModifiedOn DateTime Last modification

WidgetMode (Extensible Enum)

WidgetMode controls user interaction level:

Known Value API Value Description
Managed managed Cloudflare decides when to challenge
NonInteractive non-interactive Challenge runs without visible interaction
Invisible invisible Completely invisible

ClearanceLevel (Extensible Enum)

ClearanceLevel controls challenge difficulty:

Known Value API Value Description
NoClearance no_clearance No challenge - only monitors for bot patterns
JsChallenge jschallenge JavaScript challenge execution
Managed managed Cloudflare manages difficulty based on risk
Interactive interactive Always requires explicit user interaction

CreateTurnstileWidgetRequest

Property Type Description
Name string Widget name
Domains IReadOnlyList<string> Allowed domains
Mode WidgetMode Challenge mode
BotFightMode bool? Enable bot fight mode
Region TurnstileRegion? Processing region

RotateWidgetSecretResult

Property Type Description
Secret string New secret key

Common Patterns

Find Widget by Name

public async Task<TurnstileWidget?> FindWidgetByNameAsync(
    string accountId,
    string name)
{
    await foreach (var widget in cf.Turnstile.ListAllWidgetsAsync(accountId))
    {
        if (widget.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
        {
            return widget;
        }
    }

    return null;
}

Create Widget If Not Exists

public async Task<TurnstileWidget> EnsureWidgetExistsAsync(
    string accountId,
    string name,
    IEnumerable<string> domains)
{
    var existing = await FindWidgetByNameAsync(accountId, name);

    if (existing is not null)
    {
        return existing;
    }

    return await cf.Turnstile.CreateWidgetAsync(accountId,
        new CreateTurnstileWidgetRequest(
            Name: name,
            Domains: domains.ToList(),
            Mode: WidgetMode.Managed
        ));
}

Rotate All Secrets

public async Task<Dictionary<string, string>> RotateAllSecretsAsync(
    string accountId)
{
    var newSecrets = new Dictionary<string, string>();

    await foreach (var widget in cf.Turnstile.ListAllWidgetsAsync(accountId))
    {
        var result = await cf.Turnstile.RotateSecretAsync(accountId, widget.Sitekey);
        newSecrets[widget.Name] = result.Secret;

        Console.WriteLine($"Rotated secret for: {widget.Name}");
    }

    return newSecrets;
}

Client-Side Integration

After creating a widget, use the sitekey in your HTML:

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="YOUR_SITEKEY"></div>

Then validate the token server-side using the secret.

Required Permissions

Permission Scope Level
Turnstile Account Read (for listing/get)
Turnstile Account Write (for create/update/delete/rotate)