1---
2title: Turnstile CAPTCHA
3description: Manage Cloudflare Turnstile widgets as resources — deploy a widget, embed its sitekey in HTML, and verify tokens server-side with the redacted secret.
4sidebar:
5 order: 23
6---
7
8Turnstile is Cloudflare's CAPTCHA alternative. A Turnstile widget is
9defined by the hostnames it runs on and its interaction mode;
10Cloudflare assigns two keys in return — a public `sitekey` you embed
11in HTML and a `secret` your server uses to verify the tokens the
12widget produces.
13
14## Create a widget
15
16```typescript
17// alchemy.run.ts
18import * as Alchemy from "alchemy";
19import * as Cloudflare from "alchemy/Cloudflare";
20import * as Effect from "effect/Effect";
21
22export default Alchemy.Stack(
23 "SignupApp",
24 {
25 providers: Cloudflare.providers(),
26 state: Cloudflare.state(),
27 },
28 Effect.gen(function* () {
29 const widget = yield* Cloudflare.Turnstile.Widget("SignupForm", {
30 domains: ["example.com"],
31 mode: "managed",
32 });
33
34 return {
35 sitekey: widget.sitekey,
36 };
37 }),
38);
39```
40
41`domains` lists the hostnames the widget may run on — subdomains are
42covered automatically. `mode` is `managed` (Cloudflare decides
43whether an interaction is required), `non-interactive` (never asks),
44or `invisible` (no visible widget). If you omit `name`, a unique one
45is generated from the app, stage, and logical ID.
46
47## The two keys
48
49```typescript
50const sitekey = widget.sitekey; // public — render it in your HTML
51const secret = widget.secret; // Redacted<string> — server-side only
52```
53
54The `sitekey` is public and safe to expose as a stack output. The
55`secret` is a `Redacted<string>`: it never prints in logs or
56JSON-serialized output (it renders as `"<redacted>"`), so unwrap it
57with `Redacted.value` at the point where your server actually needs
58the raw string.
59
60## Embed the sitekey
61
62```html
63<script
64 src="https://challenges.cloudflare.com/turnstile/v0/api.js"
65 async
66 defer
67></script>
68<div class="cf-turnstile" data-sitekey="<sitekey from the deploy output>"></div>
69```
70
71The widget runs the challenge in the browser and puts the resulting
72token in a hidden `cf-turnstile-response` form field (or hands it to
73your callback). The token proves nothing by itself — your server must
74verify it.
75
76## Verify tokens server-side
77
78There is no runtime binding for Turnstile — verification is a plain
79HTTPS call from any backend. POST the token together with
80`Redacted.value(secret)` to Cloudflare's
81`https://challenges.cloudflare.com/turnstile/v0/siteverify` endpoint
82and check `success` in the response; see Cloudflare's
83[server-side validation docs](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/)
84for the full request/response contract. A Worker is a natural place
85to do this — pass the secret in via the Worker's `env`.
86
87## Updates and replacement
88
89```typescript
90const widget = yield* Cloudflare.Turnstile.Widget("SignupForm", {
91 domains: ["example.com", "app.example.com"],
92 mode: "invisible",
93});
94```
95
96Name, domains, mode, and the clearance settings all mutate in place —
97the widget keeps its `sitekey` and `secret`, so deployed pages keep
98working. Only `region` (default `"world"`) forces a replacement,
99which mints a new sitekey and secret. If the widget is deleted
100out-of-band, the next deploy observes it missing and recreates it
101rather than failing.
102
103:::note
104`botFightMode`, `ephemeralId`, and `offlabel` (hide Cloudflare
105branding) are Enterprise-only settings; `clearanceLevel` controls the
106challenge clearance granted when the widget is embedded on a
107Cloudflare zone.
108:::
109
110## Where next
111
112- [Workers](/cloudflare/compute/workers) — host your siteverify call next to
113 the form it protects.
114- [Secrets & env](/cloudflare/security/secrets-env) — how the widget secret
115 gets into the Worker.
116- [Widget API reference](/providers/cloudflare/turnstile/widget)