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 ships a four-layer protection model. As an integrator, you only need to handle two response shapes in your client:
  • 429 Too Many Requests — back off and retry after the server-provided delay.
  • challenge_required — render a Turnstile widget, collect the token, and resubmit.
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 — Per-app limit

Layer 2 is sized for aggregate traffic across your entire user base, so it comfortably accommodates normal usage patterns.

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.

Example — SIWE with inline challenge handling

The same challenge-handling pattern applies to siws and email providers — only the follow-up step after receiving the nonce or magic link differs.

Response matrix — POST /auth/start

Standalone bot protection

The /bot-protection/* endpoints let you use the same Turnstile widget to gate any action in your product you want to shield a sensitive action from automated abuse. The API surface is exactly two endpoints: fetch the site key, verify the token.

GET /bot-protection/site-key

Returns the Turnstile site key registered for your app so you can render the widget in any part of your UI.
string
required
Your application ID.
string | null
The Turnstile site key to pass into the widget as data-sitekey (or the equivalent prop in a React/Vue wrapper). Returns null when no widget is registered for the app.

POST /bot-protection/verify

Verifies a Turnstile token against the app’s linked widget. Tokens are single-use — a second verification of the same token always fails.
string
required
Your AGG app ID. Must match the app the API key was issued for.
string
required
Your app-scoped API key
string
required
The token returned by the Turnstile widget. Maximum 2048 characters.

Example — gating any action

Site key lookup happens in the browser (public, safe). Token verification happens on your server (protected by your API key). Frontend — collect the token and post it to your own backend:
Backend — verify the token with your API key before executing the action:
Need to create an API key or looking for the full reference on x-app-api-key (scopes, rotation, requireApiKey lockout, per-key rate 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.