Most people meet GeeTest v3 as "the slider one" — drag the puzzle piece into the gap, done. So the
obvious automation is: find the gap with OpenCV, compute the offset, move the slider that many
pixels. That works exactly once, in a demo, and then fails everywhere real.
The reason is that the gap offset is not what GeeTest is checking. It checks how you got
there, and then it checks again on the server.
The actual flow
GeeTest v3 is a three-step handshake, and the puzzle you see is only step two.
-
Registration. The site's backend asks GeeTest for a challenge and gets back a
gt(a public site key, stable per site) and achallenge(a one-time nonce). The page hands both to the GeeTest JS. -
The challenge. The JS renders the slider, and while you drag it, records a trajectory —
a time-series of positions, plus timing, acceleration, and a few environment signals. On
release, it posts that trajectory to GeeTest's
ajax/validateendpoint. -
Validation. If GeeTest accepts, it returns three values:
geetest_challenge,geetest_validate,geetest_seccode. Those go to the site's own backend, which calls GeeTest server-side to confirm them.
Step 3 is where most automation dies. You can get the slider visually correct and still be rejected,
because the trajectory you produced was not plausible, and the seccode never validates.
Why a pixel-perfect slide still fails
The offset is necessary but nowhere near sufficient. What gets you flagged:
-
Linear motion. A human accelerates, overshoots slightly, corrects, and settles. A
forloop moving 3px per tick produces a straight line with constant velocity — which is the single clearest tell there is. - Zero jitter on the Y axis. Real drags wander vertically by a few pixels. Machine drags don't move on Y at all.
- Impossible timing. A 200px drag completed in 40ms, or one that starts the instant the puzzle renders with no reaction delay.
-
A stale
challenge. The nonce is single-use and short-lived. Reusing one across retries — or taking 90 seconds of OpenCV work before submitting — invalidates it. -
Environment mismatch.
navigator.webdriver, a headless UA, or a TLS fingerprint that disagrees with the browser you claim to be, all feed into the same decision.
There is also a v3 vs v4 trap worth naming: GeeTest v4 is a different product with a different
handshake (captcha_id rather than gt/challenge, and a different validation payload). Guides
and libraries for one do not work against the other, and a lot of confusion online comes from
people mixing them. Check which one you're facing before you write any code —
if you see gt= and challenge= in the network tab, you're on v3.
Detecting which one you're on
import re, requests
html = requests.get(target_url, timeout=20).text
# v3 registers with a gt + challenge pair
v3 = re.search(r'["\']?gt["\']?\s*[:=]\s*["\']([a-f0-9]{32})', html)
# v4 uses captchaId / captcha_id
v4 = re.search(r'captcha[_]?[Ii]d["\']?\s*[:=]\s*["\']([a-f0-9]{32})', html)
print("GeeTest v3" if v3 else "GeeTest v4" if v4 else "neither / dynamically loaded")
If neither matches, the parameters are being fetched by JS at runtime — open the network tab and
look for the request to api.geetest.com/gettype.php or register.php; the gt is in there.
Handling it in automation
Two approaches actually work, and they fail for different reasons.
Drive a real browser and produce a real trajectory. Feasible, and the right call if you're
already running Playwright with a good fingerprint. The work is in the motion model: ease-in/ease-out
velocity, a small overshoot and correction near the target, a few pixels of Y drift, and a human
reaction delay before the drag starts. Budget for tuning — a motion curve that passes today can
start failing after a GeeTest model update, and you find out through a rising failure rate rather
than an error message.
Send the parameters to a solving service and get the three values back. You extract gt and
challenge, submit them, and receive geetest_challenge / geetest_validate / geetest_seccode
to post to the site's backend yourself. This is what we do at CaptchaAI —
the API is 2Captcha-compatible, so if you already have solver code the change is the base URL:
import json, requests, time
API = "https://ocr.captchaai.com"
KEY = "YOUR_KEY"
r = requests.get(f"{API}/in.php", params={
"key": KEY,
"method": "geetest",
"gt": gt, # public site key, usually static
"challenge": challenge, # one-time nonce — grab it fresh, see below
"pageurl": target_url,
"json": 1,
}, timeout=30).json()
task = r["request"]
time.sleep(15) # geetest takes a while; polling sooner just burns requests
while True:
res = requests.get(f"{API}/res.php", params={
"key": KEY, "action": "get", "id": task, "json": 1
}, timeout=30).json()
if "seccode" in res: # solved: challenge / validate / seccode come back top-level
sol = res
break
if res.get("status") == 1: # the same three values, if your client gets them wrapped
sol = res["request"]
sol = json.loads(sol) if isinstance(sol, str) else sol
break
if res.get("request") != "CAPCHA_NOT_READY":
raise RuntimeError(res.get("request"))
time.sleep(5)
# Note the rename: the API returns short keys, but the SITE expects them prefixed.
payload = {
"geetest_challenge": sol["challenge"],
"geetest_validate": sol["validate"],
"geetest_seccode": sol["seccode"],
}
The detail people miss: fetch the challenge immediately before you submit. If your code
registers the challenge, then does other work, then solves, the nonce may already be dead and
you'll get a valid-looking seccode that the site rejects — which reads like a solver problem and
isn't one.
The failure mode to watch
GeeTest doesn't usually tell you why it refused. You get a rejection and a fresh puzzle. So the
signal you actually need is first-attempt success rate over time, not per-request errors. A
slow slide from 95% to 80% over a week is the shape of a model update meeting a motion curve that
used to pass — and if you're only looking at exceptions, you won't see it until it's severe.
Whichever route you take, log the rate. It's the only early warning either approach gives you.
I'm Bassem, founder of CaptchaAI. We solve GeeTest v3, reCAPTCHA v2 and
v3 including Enterprise, Cloudflare Turnstile and Challenge, and image/grid types. We do **not*
solve GeeTest v4, hCaptcha or FunCaptcha — worth saying plainly, because picking a solver on a
capability it doesn't have wastes a week. If Cloudflare is your other wall, I've written up
why cf_clearance keeps expiring
and how to solve Turnstile in Playwright.
To run the GeeTest v3 call above against your own target, start with
one free thread for 30 days, no card needed.*
Top comments (0)