Repository navigation
CloudflareTurnstile API
Client configuration and the verify method of
@tundraconnect/cloudflare-turnstile.
import { CloudflareTurnstile } from '@tundraconnect/cloudflare-turnstile';
const turnstile = new CloudflareTurnstile({
auth: { type: 'CUSTOM', secretKey: 'YOUR_SECRET_KEY' },
timeout: 5,
});| Option | Type | Required | Default | Description |
|---|---|---|---|---|
auth |
CloudflareTurnstileAuth |
yes | — |
{ type: 'CUSTOM', secretKey }, the widget's secret key. |
timeout |
number |
no | 10 |
Default per-call deadline in seconds, 1–120, fractions allowed. |
baseURL |
string |
no | https://challenges.cloudflare.com/turnstile/v0 |
Override for a proxy or a test double. |
auth missing, not CUSTOM, or with a blank secretKey →
CONFIG_INVALID_SECRET_KEY at construction. The secret is sent in the
request body, never a header or the URL. RESTler drops request bodies from
its call events and error contexts, so the secret never appears in either.
| Getter | Type | Description |
|---|---|---|
vendor |
string |
Always 'CloudflareTurnstile'. |
| Export | Value |
|---|---|
TURNSTILE_API |
https://challenges.cloudflare.com/turnstile/v0 |
TURNSTILE_DUMMY_SECRETS |
{ alwaysPasses, alwaysFails, alreadySpent } — Cloudflare's documented dummy secret keys. |
TURNSTILE_DUMMY_TOKEN |
XXXX.DUMMY.TOKEN.XXXX — the token dummy sitekeys hand the browser. |
POST /siteverify with a JSON body → VerificationSchema.
| Field | Type | Required | Description |
|---|---|---|---|
response |
string |
yes | The widget token, at most 2048 characters. |
remoteip |
string |
no | The visitor's IP, when you have it. |
idempotency_key |
string |
no | A UUID that makes re-verifying the same token return the original verdict. |
expectedHostname |
string | readonly string[] |
no | Hostname(s) the widget is served on; compared case-insensitively with the verdict's. |
expectedAction |
string |
no | The widget's data-action; compared exactly with the verdict's. |
timeout |
number |
no | Deadline for this one call, 1–120 seconds. |
response, remoteip and idempotency_key are Cloudflare's own names, so
a body copied from the siteverify docs works unchanged. The secret is added
from auth.
Turnstile answers almost everything with an HTTP 200 and puts the outcome
in the body's error-codes. The connect splits those into two kinds:
error-codes |
Outcome |
|---|---|
none (success: true) |
resolves, after the expectation checks below |
invalid-input-response, timeout-or-duplicate
|
resolves as success: false — about the token |
missing-input-secret, invalid-input-secret
|
throws AUTH_FAILED
|
bad-request, missing-input-response
|
throws INVALID_REQUEST
|
internal-error |
throws SERVICE_UNAVAILABLE (transient) |
When a response carries several codes, the secret ones take precedence,
then the request ones, then internal-error. An HTTP error status is
classified by status (401/403 → AUTH_FAILED, 429 →
RATE_LIMITED, 5xx → SERVICE_UNAVAILABLE, other 4xx →
INVALID_REQUEST).
Cloudflare recommends checking hostname and action on every verdict —
a token is only proof that some widget was solved. When expectedHostname
or expectedAction is set and Cloudflare said success: true:
- a verdict whose
hostnameis not in the expected list (or is absent) is returned withsuccess: falseandhostname-mismatchappended toerror-codes; - a verdict whose
actiondiffers is returned withsuccess: falseandaction-mismatchappended.
hostname-mismatch and action-mismatch are added by this connect;
Cloudflare never emits them. A verdict Cloudflare already failed is returned
as-is. The rest of the verdict (challenge_ts, hostname, action,
cdata, metadata) is kept for diagnostics either way.
import { CloudflareTurnstile } from '@tundraconnect/cloudflare-turnstile';
declare const turnstile: CloudflareTurnstile;
declare const token: string;
const verdict = await turnstile.verify({
response: token,
expectedHostname: ['example.com', 'www.example.com'],
expectedAction: 'login',
});
if (!verdict.success) {
console.log(verdict['error-codes']); // e.g. ['hostname-mismatch']
}Cloudflare's test secrets answer with hostname: 'example.com', no
action, and metadata.result_with_testing_key: true. An
expectedHostname other than example.com, or any expectedAction,
therefore turns a test-key pass into success: false with
hostname-mismatch / action-mismatch. Set the expectations only in
production, or branch on verdict.metadata?.result_with_testing_key.
A token is single-use: a second verification answers
timeout-or-duplicate. Send the same idempotency_key with every attempt
of one verification to make retries safe.
The widget management API (/accounts/{account_id}/challenges/widgets —
list, create, rotate secret, …) is a Cloudflare account API authenticated
with an API token and wrapped in the client/v4 envelope. It is a different
credential and a different envelope from siteverify, and is left out.
Subclass and reassign the protected _fetch:
import {
CloudflareTurnstile,
type CloudflareTurnstileOptions,
} from '@tundraconnect/cloudflare-turnstile';
class RoutedTurnstile extends CloudflareTurnstile {
constructor(options: CloudflareTurnstileOptions, transport: typeof fetch) {
super(options);
this._fetch = transport;
}
}