Documentation Index
Fetch the complete documentation index at: https://docs.agg.market/llms.txt
Use this file to discover all available pages before exploring further.
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.
| Layer | Purpose | Client-visible outcome |
|---|---|---|
| 1 | Per-IP rate limit across every endpoint | 429 Too Many Requests |
| 2 | Per-app rate limit across every endpoint | 429 Too Many Requests |
| 3 | Tighter rate limit on POST /auth/start | 429 Too Many Requests |
| 4 | Cloudflare Turnstile challenge on sign-in | challenge_required → 403 on invalid token |
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
| Property | Value |
|---|---|
| Scope | All endpoints |
| Default | 100 requests / 60 seconds per IP |
| Response | 429 Too Many Requests with Retry-After header (seconds) |
Layer 2 — Per-app limit
| Property | Value |
|---|---|
| Scope | All endpoints that carry x-app-id |
| Default | 500 requests / 60 seconds per app |
| Response | 429 Too Many Requests with Retry-After header |
Layer 3 — Sign-in limit
A tighter per-IP limit applied only toPOST /auth/start. Authentication endpoints are
high-value targets for credential stuffing, nonce mining, and email enumeration, so they
warrant stricter throttling.
| Property | Value |
|---|---|
| Scope | POST /auth/start only |
| Default | 20 requests / 60 seconds per IP |
| Response | 429 Too Many Requests with retryAfter in the response body |
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.
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:- The sign-in provider is
siwe,siws, oremail(OAuth providers are exempt). - The app has
botProtectionEnabled = true. - The app’s registered user count is at or above
botProtectionThreshold. - A Turnstile widget is linked to the app.
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
siws and email providers — only the
follow-up step after receiving the nonce or magic link differs.
Response matrix — POST /auth/start
| Scenario | HTTP | Body |
|---|---|---|
| Below threshold, or bot protection disabled | 200 | { type: "nonce" | "magic_link" | ... } |
Challenge required, no turnstileToken submitted | 200 | { type: "challenge_required", siteKey } |
Challenge required, valid turnstileToken | 200 | { type: "nonce" | "magic_link" | ... } |
| Challenge required, invalid or reused token | 403 | { message: "Bot protection verification failed." } |
| Rate limit breached | 429 | { message: "Too many requests. Retry after Xs.", retryAfter } |
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.
Your application ID.
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.
Your AGG app ID. Must match the app the API key was issued for.
Your app-scoped API key
The token returned by the Turnstile widget. Maximum 2048 characters.
| Response | Meaning |
|---|---|
200 { "success": true } | Token is valid — proceed with the gated action. |
400 Bot protection is not configured for this app. | No widget is linked to the app. |
401 Invalid API key | Missing, malformed, revoked, or expired x-app-api-key, or x-app-id does not match the key’s app. |
403 Invalid Turnstile token. | Empty, oversize, or malformed token. |
403 Bot protection verification failed. | Cloudflare rejected the token or it was already consumed. |
429 | Per-IP or per-app rate limit exceeded. |
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: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.Rendering the Turnstile widget
Rendering the widget is a three-step process on the client:- Load the Turnstile script — Cloudflare’s loader (
api.js) exposes a globalwindow.turnstileobject with the widget API. - Mount the widget into a DOM element — pass the
siteKeyfetched fromGET /bot-protection/site-key(or thesiteKeyreturned in achallenge_requiredresponse). - 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:
_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 classcf-turnstile and auto-renders a widget into it using the attributes
you provide:
Step 3 — Programmatic rendering (React)
For SPAs — where the widget is mounted and unmounted as the user navigates — use the programmatic API. Callwindow.turnstile.render() to mount, reset() when the user
retries, and remove() on unmount so the widget doesn’t leak across route changes.
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/startor/bot-protection/verifycannot 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.
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
| Symptom | Likely cause |
|---|---|
403 Bot protection verification failed. on first submit | Token expired (over 5 minutes since solve), or the widget domain is not registered for this app. |
403 on a follow-up submit with the same token | Token was already consumed — collect a fresh one from the widget. |
400 Bot protection is not configured for this app. | The app has no Turnstile widget linked. Re-enable via the operator endpoint, or contact AGG support. |
429 on repeated /auth/start calls | Layer 3 sign-in rate limit. Back off and retry after the retryAfter interval. |
| Widget never renders in the browser | No widget is registered for the app — GET /bot-protection/site-key returns { siteKey: null }. |
Related
Authentication
Full sign-in flow for SIWE, SIWS, email, and OAuth providers.
Token Refresh
Renew access tokens after a successful sign-in.