Skip to main content
Bot protection is enabled by default on every new app. A Cloudflare Turnstile widget is provisioned automatically at app-creation time (provided the app has at least one allowedOrigin), so you can start using it immediately — no setup required. This guide covers the full protection model, the standalone /bot-protection/* endpoints you can use to gate any action in your product, and how to render the Turnstile widget in your frontend.

Overview

AGG combines global identity quotas, scoped read quotas, sign-in limits, and Turnstile challenges. Handle these response shapes in your client:
  • 429 Too Many Requests — back off and retry after the server-provided delay.
  • 503 Service Unavailable — scoped rate-limit enforcement is unavailable; honor its retry hint.
  • challenge_required — render a Turnstile widget, collect the token, and resubmit.
Market-data reads also have automatic route-family quotas, including an outcome-unit budget shared by the single and batch outcome snapshot routes. Layers 1–3 are automatic: they run before your request reaches any handler and return a standard 429 you can retry. Layer 4 is the Turnstile challenge, which you integrate once and reuse anywhere in your product.

Rate limits

Layer 1 — Per-IP limit

Layer 2 — Browser identity or validated API key

This is not a shared limit for every user of an app. Browser requests must satisfy both the IP and identity limits. A validated API key uses its own limit instead, with an 18,000-request default or a configured per-key quota, and is exempt from the browser IP limit. New market-data route caps still apply to that key and are not bypassed by a higher per-key quota. If scoped market-data limits cannot be enforced, those routes return 503 with Retry-After: 1 and Cache-Control: private, no-store; retry after the hint.

Layer 3 — Sign-in limit

A tighter per-IP limit applied only to POST /auth/start. Authentication endpoints are high-value targets for credential stuffing, nonce mining, and email enumeration, so they warrant stricter throttling.

Handling 429 on the client

Always honor the server’s retry hint: Retry-After header for Layers 1 and 2, retryAfter body field for Layer 3. Never hardcode a fixed delay.
A burst of 429 responses during normal use typically indicates a tight retry loop in your client. Debounce the caller and surface a visible “please wait” state after the second consecutive 429.

Turnstile challenges

Layer 4 activates when all of the following are true:
  1. The sign-in provider is siwe, siws, or email (OAuth providers are exempt).
  2. The app has botProtectionEnabled = true.
  3. The app’s registered user count is at or above botProtectionThreshold.
  4. A Turnstile widget is linked to the app.
When activated, POST /auth/start returns { type: "challenge_required", siteKey } instead of the usual nonce or magic link. Your client renders the Turnstile widget, collects the resulting token, and retries /auth/start with turnstileToken included in the body. When type is challenge_required, render the widget and send the same start again with turnstileToken. Then continue Quickstart: REST. The same retry applies to siws and email. Wallet message signing stays on that quickstart.

Response matrix — POST /auth/start

Standalone bot protection

Use the same widget to gate any action. Two calls: fetch the site key from the browser, then verify the token from your server.
Verify requires both x-app-id and x-app-api-key. The server checks that x-app-id matches the app id embedded in the key. An API key alone is not enough. Tokens are single-use.
Request fields are on Verify Turnstile token.

Example — gating any action

Site key lookup happens in the browser. Token verification happens on your server, with both headers. Frontend — collect the token and post it to your own backend:
Need an API key, or the rules for x-app-api-key (scopes, rotation, requireApiKey, per-key limits)? See Server API Keys.
Each Turnstile token can only be verified once. If you pre-verify a token against /bot-protection/verify and then submit the same token to another endpoint, the second call will fail. For any given user action, collect a fresh token from the widget before submitting.

Rendering the Turnstile widget

Rendering the widget is a three-step process on the client:
  1. Load the Turnstile script — Cloudflare’s loader (api.js) exposes a global window.turnstile object with the widget API.
  2. Mount the widget into a DOM element — pass the siteKey fetched from GET /bot-protection/site-key (or the siteKey returned in a challenge_required response).
  3. Collect the token in a callback — the widget fires a callback with a short-lived token once the user passes the challenge. Submit that token alongside the user’s action.

Step 1 — Load the Turnstile script

Add the loader to your document <head> once, on every page that might render a widget. The async defer attributes keep it from blocking initial render:
In a React/Next.js app, load it once in your root layout (e.g. _app.tsx or layout.tsx) using Next’s <Script> component or a one-shot effect:

Step 2 — Declarative embed (simplest)

The quickest path is the declarative embed. Cloudflare’s loader scans the DOM for any element with class cf-turnstile and auto-renders a widget into it using the attributes you provide:
Use this when the widget lives on a static page (a sign-in form, a contact form, etc.) and you’re happy to read the token from a callback.

Step 3 — Programmatic rendering (React)

For SPAs — where the widget is mounted and unmounted as the user navigates — use the programmatic API. Call window.turnstile.render() to mount, reset() when the user retries, and remove() on unmount so the widget doesn’t leak across route changes.
Wire it into a form component:

Token lifetime and reset behavior

Tokens are short-lived and single-use:
  • 5-minute TTL — Cloudflare invalidates a token 5 minutes after the user solves the challenge.
  • Single-use — AGG enforces one-time verification. A token consumed by /auth/start or /bot-protection/verify cannot be reused.
  • Always collect a fresh token right before the submit action, not at page load.
  • On a failed submit, call turnstile.reset() so the user can produce a new token without a full page reload.
Cloudflare also fires expired-callback if the user leaves the widget idle too long — handle it by clearing your local token state so the submit button re-disables until the user re-solves.

Troubleshooting

Authentication

Full sign-in flow for SIWE, SIWS, email, and OAuth providers.

Token Refresh

Renew access tokens after a successful sign-in.