Documentation Index
Fetch the complete documentation index at: https://mintlify.com/midudev/mundial-de-clicks/llms.txt
Use this file to discover all available pages before exploring further.
POST /api/captcha/redeem is the final step of the Cap captcha flow. The browser submits the solved proof-of-work payload it received from the Cap widget. This endpoint proxies that payload to the Cap standalone server for verification. If verification succeeds, the server creates a vote session in DragonFly and responds with two Set-Cookie headers: cap_session (the short-lived vote quota cookie) and voter_id (a persistent identity cookie lasting 180 days). Only after this exchange can the browser start casting votes.
Prerequisites
Two conditions must be met before the endpoint accepts any request:- Captcha must be enabled:
CAP_API_URLmust be set and DragonFly must be reachable. If not, returns404 { "error": "captcha_disabled" }. VOTER_ID_SECRETmust be configured: the voter identity cookie is HMAC-signed; without a signing key the endpoint cannot issue it. Returns503 { "error": "voter_id_secret_missing" }if missing. In non-production environments, a hardcoded development secret is used as a fallback.
Request
The proof-of-work solution produced by the Cap widget after the user completes the puzzle.
The challenge token originally issued by
POST /api/captcha/challenge. The Cap server uses this to match the solution to the original puzzle.{CAP_API_URL}/redeem. Any extra fields the Cap widget includes are passed through unchanged.
Rate limiting
The endpoint applies a per-IP rate limit ofCAP_REDEEM_MAX_PER_MINUTE (default 12) requests per 60-second window, enforced via consumeAbuseLimit. When the limit is exceeded, a retry-after header is included in the 429 response:
Response on success
Status:200 OK
Short-lived session cookie. Value is a UUID v4 that maps to two DragonFly keys:
cap:sess:{id}— remaining vote quota, initialised tomin(CAP_VOTES_PER_SESSION, CAP_SESSION_HARD_VOTE_CAP)(default50).cap:sess-ip:{id}— the trusted client IP bound at creation time.
Path=/; HttpOnly; SameSite=Lax; Max-Age={SESSION_TTL} where SESSION_TTL = max(30, CAP_SESSION_TTL_SECONDS) (default 120 s; the floor of 30 prevents accidental sub-second TTLs from a misconfigured env var). Each accepted vote decrements the quota and renews the TTL. The session expires by time or by exhausting the quota, whichever comes first. The Secure flag is omitted intentionally — the app may be served over plain HTTP (e.g. sslip.io without TLS).Persistent identity cookie. Format:
{uuid}.{base64url-hmac}. The UUID is either read from an existing valid voter_id cookie (preserving continuity across sessions) or freshly generated. The HMAC is computed with HMAC-SHA256 using VOTER_ID_SECRET (falling back to ORIGIN_GUARD_SECRET).Cookie attributes: Path=/; HttpOnly; SameSite=Lax; Max-Age=15552000 (180 days). Survives browser restarts. Used as a secondary daily vote cap alongside the IP-based cap — a client who changes IPs cannot reset their daily allowance while keeping the same voter_id.Response on Cap verification failure
If the Cap server returns a non-200 status or a body wheresuccess is not true, the endpoint relays the Cap server’s response directly without creating a session or setting cookies:
400).
Session creation details
On successful verification,createSession(ip) executes an atomic MULTI/EXEC pipeline against DragonFly:
SESSION_TTL = max(30, CAP_SESSION_TTL_SECONDS) (default 120 s).
Both keys share the same TTL and are set atomically. The UUID returned becomes the cap_session cookie value.
The IP stored in
cap:sess-ip:{id} is read from captchaSessionIp(request), which calls getTrustedClientIp — it reads CF-Connecting-IP in production (Cloudflare), or falls back to X-Real-IP in non-production environments. It never reads X-Forwarded-For to prevent IP spoofing.Error responses
| Status | Body error field | Cause |
|---|---|---|
400 | invalid_body | Request body is not valid JSON |
403 | trusted_ip_required | CF-Connecting-IP is absent (production) or no usable IP can be determined |
404 | captcha_disabled | CAP_API_URL not set or DragonFly unavailable |
429 | rate_limited | Exceeded CAP_REDEEM_MAX_PER_MINUTE in the current window |
502 | captcha_unreachable | Cap server did not respond within CAP_HTTP_TIMEOUT_MS |
503 | voter_id_secret_missing | VOTER_ID_SECRET (and ORIGIN_GUARD_SECRET) are both unset in production |
503 | rate_limit_unavailable | DragonFly threw while incrementing the abuse counter |
Environment variables
| Variable | Default | Effect |
|---|---|---|
CAP_API_URL | (unset) | Base URL of the Cap standalone server. Required for captcha to be active. |
VOTER_ID_SECRET | (unset) | HMAC key for signing the voter_id cookie. Required in production. |
ORIGIN_GUARD_SECRET | (unset) | Fallback HMAC key if VOTER_ID_SECRET is not set. |
CAP_VOTES_PER_SESSION | 50 | Vote quota granted per solved captcha. |
CAP_SESSION_HARD_VOTE_CAP | 50 | Hard upper bound on vote quota (effective quota = min of both). |
CAP_SESSION_TTL_SECONDS | 120 | Session lifetime in seconds; effective TTL is max(30, value) (reset on each accepted vote). |
CAP_REDEEM_MAX_PER_MINUTE | 12 | Per-IP redeem rate limit. |
CAP_HTTP_TIMEOUT_MS | 5000 | Hard timeout (ms) for the server-to-Cap fetch. |