Dev.to Security 🔐 Cybersecurity 👁 0 📖 4 min read

FunCaptcha (Arkose Labs): why the token is bound to your session — and the three ways people break it

FunCaptcha used to be the easy one. You found the public key in the page, you called the endpoint, you got a token, you posted it back. That stopped working around the time Arkose Labs started calling the product "Arkose

FunCaptcha used to be the easy one. You found the public key in the page, you called the endpoint, you got a token, you posted it back. That stopped working around the time Arkose Labs started calling the product "Arkose Bot Manager" and putting an enforcement endpoint in front of every challenge.

The token is still there. What changed is that the token is no longer a standalone answer to a puzzle — it is the tail end of a session that started before you ever saw a challenge, and every field in it is cross-checked against what the browser actually did.

This post is about that session, the three ways a solve breaks, and how to read the failure instead of retrying it.

What a FunCaptcha solve actually consists of

Three things, in order:

  1. A public key (arkose_labs_..., or the older UUID form) that identifies the site's Arkose application.
  2. A blob — the session state: encrypted risk signals collected from the page, the browser, the network, plus the enforcement decision (pass, challenge, or block). It arrives in the fc-token blob or from https://client-api.arkoselabs.com/fc/gt2/public_key/<key>.
  3. A token — what you get back after the challenge is passed. This is what your target site actually verifies.

The common mistake is to treat step 3 as the whole task. On a plain image captcha, it is. On FunCaptcha, the token is issued against the blob from step 2, and the target site's backend calls Arkose's verify endpoint with both.

The three failure modes, and what each one means

1. ERROR: Invalid token — the token was solved against a stale blob

The blob has a lifetime measured in minutes and it is bound to a session fingerprint. If you fetch it, wait, then solve, the token comes back valid-looking but the verify call rejects it.

Fix: treat blob fetch and solve as one operation. If the solve queue is slow, refetch before submitting.

2. ERROR: Session mismatch — the browser that got the blob is not the browser that submits

The blob encodes a device/browser signal set. Submitting the token from a different IP, a different user agent, or a different TLS fingerprint than the one that requested the blob fails the check even when the token itself is perfect.

Fix: one identity per session, from blob to submit. This is the same rule as DataDome (I wrote about why rotating proxies makes DataDome worse), and it applies here ten times harder because Arkose is a risk engine — a mismatched device is not "low confidence", it is a signal.

3. ERROR: Challenge not passed — the puzzle result was correct but the interaction was not

Arkose scores how the puzzle was solved: timing, pointer paths, keystroke cadence, whether the image tiles were even looked at. Two answers can be equally correct and score differently.

Fix: this is the one you cannot paper over in code. Solve with a service that handles the interaction model, or reduce the challenge rate upstream by looking less like a bot before the challenge appears.

Reading the payload before you retry

Before blaming the solver, dump what you actually sent:

import json

payload = json.loads(blob_raw)          # what the client-api returned
print(payload.get("challenge", {}).get("type"))
print(payload.get("session", {}).get("id"))
print(len(payload.get("data", {}).get("blob", "")))
Symptom Likely cause What to change
Token rejected instantly Blob expired between fetch and submit Fetch, solve, submit in one pass
Token rejected after the site's own verify IP / UA / TLS changed mid-session Pin the identity per session
challenge not passed on a correct answer Interaction score too low Stop scripting the puzzle; change the upstream footprint
No challenge at all, straight to block Risk score already too high The problem is before FunCaptcha, not in it

The last row is the one people miss. If Arkose returns a hard block instead of a challenge, no solver will help, because there is no challenge to solve. That is a proxy reputation and request-shape problem.

A minimal working flow

import requests

API = "https://api.example-solver.com/v1/tasks"   # your solver endpoint
HEADERS = {"Authorization": f"Bearer {TOKEN}"}

task = requests.post(API, headers=HEADERS, json={
    "type": "funcaptcha",
    "websiteURL": target_url,
    "websitePublicKey": public_key,
    "data": {"blob": blob_raw},
}).json()

# poll
import time
for _ in range(60):
    r = requests.get(f"{API}/{task['taskId']}", headers=HEADERS).json()
    if r["status"] in ("ready", "failed"):
        break
    time.sleep(2)

print(r["status"], r.get("solution", {}).get("token"))

Two notes that save hours:

  • Pass the blob as data.blob, not as a query param on the public key. The verify endpoint compares them.
  • Keep the polling interval at 2–5s. Hammering the status endpoint gets you rate-limited by the solver and by Arkose, and the second one leaves a mark on the session.

Why this matters more in 2026

Arkose's enforcement API now runs on a large share of the sites that used to be plain reCAPTCHA — ticketing, gaming logins, account recovery. The challenge itself is a small part of the defence; the session and the risk score around it are the rest. Code that treats "get token" as the interface will keep working on easy sessions and keep failing on the ones you actually care about.

If you want a reference implementation rather than writing your own, I maintain SolveHub (I'm the author) — it takes type: "funcaptcha" with the same blob field shape shown above, and bills per completed task rather than per attempt, which matters when you are debugging sessions rather than puzzles.

Next in this series: PerimeterX / HUMAN — the _px3 cookie, and why the payload you send is checked against the page you claim to be on.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.