/password/login, or burn through SMS quotas via /phone/send-code. Pylon’s built-in CAPTCHA gate sits in front of those endpoints; configure one env var pair and unauthenticated bots get bounced before they reach the rate limiter.
Three providers ship in the box:
- hCaptcha — independent, privacy-focused, free up to ~1M/month
- Cloudflare Turnstile — invisible challenge, free for any volume
- Google reCAPTCHA — v2 + v3 token shapes
Gated endpoints
WhenPYLON_CAPTCHA_PROVIDER + PYLON_CAPTCHA_SECRET are both set, these endpoints require a captchaToken in the request body:
When the env vars are unset, the gate is skipped entirely — existing apps keep working unchanged.
Configuration
turnstileandcloudflareboth select Turnstile.recaptchaandgoogleboth select reCAPTCHA.
siteverify endpoint with the supplied token + the request’s peer IP:
- hCaptcha:
https://api.hcaptcha.com/siteverify - Turnstile:
https://challenges.cloudflare.com/turnstile/v0/siteverify - reCAPTCHA:
https://www.google.com/recaptcha/api/siteverify
PYLON_CAPTCHA_SECRET. Get both from the provider’s dashboard.
Client integration
cf-turnstile-response; for reCAPTCHA it’s g-recaptcha-response. All three resolve to a single captchaToken field on the wire to Pylon.
Verify behavior
Missing or invalid token returns400 CAPTCHA_FAILED:
missing-input-response, invalid-input-response) is logged server-side with tracing::warn! so operators can debug from logs without leaking which check failed back to the client.
The gate runs before the rate limiter and before any DB / email work — bots that fail the challenge don’t consume rate-limit budget or trigger downstream actions.
Where to go next
- Magic codes — primary gated endpoint
- Password — register flow uses the same gate
- Phone / SMS —
/phone/send-codeis gated identically