How to Solve GeeTest v3 in Python and Post It Back

Most CAPTCHA code assumes one token in, one token out. GeeTest breaks that. A solve returns three values that have to be submitted together, and the challenge you fed in expires roughly sixty seconds after you fetched it. Both of those catch people out, and neither fails with a helpful message.
Two inputs that behave differently
| Value | Lifetime |
|---|---|
gt | Static for the site. Cache it freely |
challenge | Single use, dead in about 60 seconds |
Both come from the site’s own GeeTest init endpoint. The gt is effectively a site identifier. The challenge is per attempt, and treating it as reusable is the single most common reason a GeeTest integration works in testing and falls apart under load.
Setup
# Python 3.10 or newer. pip install capskip
from capskip import CapSkip
solver = CapSkip(
host="127.0.0.1",
port=8080,
recaptchaTimeout=300, # GeeTest uses this, not defaultTimeout
)That timeout distinction is easy to miss. defaultTimeout only governs image CAPTCHAs; everything interactive uses recaptchaTimeout.
Solving
result = solver.geetest(
gt="81388ea1fc187e0c335c0a8907ff2625",
challenge="7cf6a8b1a2c34d5e6f7089abcdef0123",
url="https://example.com/login",
)
print(result["challenge"])
print(result["validate"])
print(result["seccode"])Those three keys are the answer. result["code"] is populated too, but for GeeTest it holds the raw JSON string rather than something you can submit, so reaching for it out of habit produces a confusing failure.
Use the challenge that comes back, not the one you sent in. They are not always the same value.
The full flow, in one place
Fetching and solving have to be adjacent. Anything slow between them risks the expiry:
import time
import requests
from capskip import CapSkip
solver = CapSkip()
LOGIN = "https://example.com/login"
# 1. Fresh pair, cache-busted. Cached init responses hand back dead challenges.
init = requests.get(
"https://example.com/geetest/init",
params={"t": int(time.time() * 1000)},
).json()
# 2. Solve immediately. Do not queue this.
result = solver.geetest(gt=init["gt"], challenge=init["challenge"], url=LOGIN)
# 3. Post all three together, alongside the real form fields.
response = requests.post(LOGIN, data={
"geetest_challenge": result["challenge"],
"geetest_validate": result["validate"],
"geetest_seccode": result["seccode"],
"username": "...",
"password": "...",
})The cache-busting timestamp matters more than it looks. GeeTest init endpoints are often cached by a CDN or a proxy, and a cached response hands you a challenge that was already spent.
Those three field names are the usual GeeTest v3 convention, but a site can rename them. Check the real form before assuming.
Doing several at once
The trap here is batching wrong. Fetching a pile of challenges and then solving them all guarantees the later ones expire before their turn. Fetch and solve inside the same task:
import asyncio
from capskip import AsyncCapSkip
async def solve_one(solver, url):
init = await fetch_pair(url) # your own fetch, awaited
return await solver.geetest(
gt=init["gt"], challenge=init["challenge"], url=url,
)
async def main():
solver = AsyncCapSkip()
return await asyncio.gather(*[solve_one(solver, u) for u in urls])
asyncio.run(main())Python is the only CapSkip SDK where AsyncCapSkip is a real async client rather than an alias, so this genuinely runs solves in parallel.
When it fails
from capskip import (
ValidationException, NetworkException, ApiException, TimeoutException,
)
try:
result = solver.geetest(gt=gt, challenge=challenge, url=page_url)
except ValidationException:
pass # missing gt or challenge
except NetworkException:
pass # CapSkip is not running
except ApiException:
pass # usually a challenge that expired before the solve finished
except TimeoutException:
pass # exceeded recaptchaTimeoutIn practice most GeeTest failures arrive as ApiException and mean the challenge died. The fix is fetching later, not retrying with the same pair, because a spent challenge never becomes valid again.
Frequently asked questions
Why is result[“code”] not usable?
Because the answer is three values rather than one. code keeps the raw JSON for completeness while the SDK expands the useful parts into challenge, validate and seccode. Submit those three.
Can I cache the challenge between runs?
No. It is single use and expires in about a minute. The gt is safe to cache; the challenge never is.
Does this cover GeeTest v4?
The geetest method targets v3, the slide puzzle built around a gt and challenge pair. v4 changed the parameter model, so check the current API documentation before assuming the same call applies.
Summary
Fetch gt and challenge immediately before solving with a cache-busted request, call geetest, then post challenge, validate and seccode together. Use the returned challenge rather than your input, and never batch challenge fetching ahead of solving.
Other languages are on the GeeTest solver page, the wider Python surface on the Python CAPTCHA solver page, and there is a live puzzle on our GeeTest v3 demo. CapSkip handles captcha bypass locally, so solve volume is free.
