How to Solve ALTCHA in Python Without Running a Browser

You can solve ALTCHA in Python with an HTTP client and nothing else. ALTCHA is proof of work rather than recognition: the site issues a challenge, and whoever wants through has to hash until they find the number that satisfies it. Nothing has to be looked at, so no browser, no WebDriver and no user agent are involved, and the answer is computed rather than guessed. That also means a solve is deterministic. Either it finds the number, in milliseconds, or the challenge itself was malformed or had already expired. CapSkip added the type in version 1.2.6 and the Python SDK exposes it as one method.
What you need
- CapSkip 1.2.6 or later on a Windows machine. That is the release ALTCHA arrived in.
- Python 3.10 or newer, and the CapSkip package.
- The URL of the page carrying the widget, plus the endpoint the widget fetches its challenge from.
- An address for the solver. Local mode listens on 127.0.0.1 and serves that device only; Server mode listens on your network address or public IP so a different machine can reach it. Both are set under connection settings.
# pip install capskip pip install capskip requests
Step 1: the solve call, and which address to point it at
Pass the page URL and the challenge endpoint. CapSkip fetches the challenge itself, hashes until it finds the counter, and hands back the payload the form wants.
# pip install capskip
from capskip import CapSkip
solver = CapSkip(host="127.0.0.1", port=8080)
# CapSkip fetches the challenge, then brute-forces the counter.
result = solver.altcha(
url="https://example.com/signup",
challenge_url="https://example.com/altcha/challenge",
)
print(result["token"]) # base64 payload for the form field
print(result["number"]) # the counter that satisfied itTwo keys in that dictionary exist for ALTCHA only. The token key holds the base64 payload, and the number key holds the counter that solved the challenge. The code key carries the same string as the token, so either one works, but the token key is named for the field it goes into.
The counter is reported for both ALTCHA generations, which is more useful than it sounds, because their payloads disagree about where it lives. A legacy payload carries it at the top level and a proof-of-work v2 payload does not, keeping it inside a solution object instead. CapSkip reads it out of the solution object in its own API response, so you get one field whichever generation the site runs.
Loopback is only right while you share a machine
That host argument is the one line to change when the script moves. 127.0.0.1 is correct while the Python process and the solver sit on the same box. Once the script runs in a container, on a VPS, on a CI runner or as a scheduled job on a managed platform, loopback points at that environment instead and the first call raises a NetworkException.
Server mode is the answer. It makes CapSkip listen on your network address or public IP rather than on loopback, so any of those can reach the same solver over the API. Use a static public IP if the traffic crosses the internet, with a firewall rule limited to the addresses you expect. Nothing else changes: the solver still runs on your own hardware and is still unmetered, so only its address is different. Read the value from the environment rather than hardcoding it, since the client already looks for CAPSKIP_HOST, CAPSKIP_PORT and CAPSKIP_API_KEY.
Step 2: where the challenge comes from, and the two ways to pass it
Open DevTools, switch to the Network tab, and reload the page. The widget makes one request for its challenge, usually to a path with altcha in it. That URL is what you pass. The JSON it returns is the challenge document, and you can pass that instead.
Read the page source rather than guessing the attribute, because it changed between widget generations.
| Widget generation | Attribute that names the challenge |
|---|---|
| v1 and v2 | challengeurl for an endpoint, with a separate challengejson attribute when the challenge is inline |
| v3 and later | challenge, and that one attribute accepts either a URL or the challenge data |
Widget styling is irrelevant here. The native, checkbox and switch variants all submit the same payload and the difference never reaches the solver, so there is nothing to detect. For the field-by-field description of a challenge, ALTCHA keeps its own widget and server documentation.
When your scraper already has the document, hand it over directly and skip the fetch entirely. The parameter takes a dictionary and serialises it for you, or a JSON string if that is what you are holding.
# The document is already here, so no request goes out.
result = solver.altcha(
url="https://example.com/signup",
challenge_json={
"algorithm": "SHA-256",
"challenge": "YOUR_CHALLENGE_HASH",
"salt": "YOUR_SALT",
"signature": "YOUR_SIGNATURE",
"maxnumber": 1000000,
},
)Passing both is allowed, and the inline document wins, because a fetch would only re-obtain what you just supplied. The two paths differ in one way that matters when work queues up: an inline challenge that has already expired is refused immediately rather than hashed pointlessly, while an endpoint lets the solver fetch a fresh challenge if the first one died while the job waited.
Which algorithms the solver covers
One method handles both generations. The legacy scheme is covered with SHA-1, SHA-256, SHA-384 and SHA-512, and proof-of-work v2 with PBKDF2 and iterative SHA. PBKDF2 is the default that ALTCHA itself recommends, so that is the large majority of live sites.
Argon2id and scrypt are the exceptions. They are refused rather than attempted: a challenge asking for either comes back in about a third of a second as ERROR_CAPTCHA_UNSOLVABLE and is never retried, because a memory-hard function is not something a retry fixes.
Step 3: post the token back untouched, before it expires
The widget submits its payload in a form field named altcha. Send the string exactly as it arrived.
import requests
# No strip(), no re-encoding, no rebuilding the JSON.
r = requests.post("https://example.com/signup", data={
"email": "someone@example.com",
"altcha": result["token"],
})That payload is base64 of a JSON document whose fields are covered by the server’s HMAC signature, so any edit invalidates it. Calling strip on it, decoding and re-encoding it, or rebuilding the dictionary with the keys in another order all produce a token the site rejects. Some integrations read it from a JSON body field rather than a form field, so check what the page’s own submit sends and match it.
If a site rejects a token your log shows as solved, an expired challenge is more likely than a corrupted one. Challenge windows are short and some close inside two minutes, and an expired challenge comes back as a bare verification failure that looks exactly like a wrong answer. Fetch, solve and submit in one unit of work, and never hold a token while a person fills in a form.
The client’s own polling timeouts are not what limits you here, since the challenge window closes long before either one does. It is still worth knowing which applies, because ALTCHA sits on the short side of the split. It is CPU work rather than a browser session, so it uses the default polling timeout and not the longer reCAPTCHA one.
| Constructor option | Default | What it covers |
|---|---|---|
| defaultTimeout | 120 seconds | ALTCHA and image CAPTCHA polling |
| recaptchaTimeout | 300 seconds | reCAPTCHA, Turnstile and GeeTest polling |
| pollingInterval | 5 seconds maximum | Polling starts at 0.25 seconds and backs off to this |
Step 4: solving a batch without every challenge going stale
Python is the one SDK where the async client is a separate implementation: AsyncCapSkip here is genuine asyncio. In the Node.js and .NET packages that name is an alias of the ordinary client, whose methods are already asynchronous, and PHP is synchronous throughout.
The constraint is not concurrency, it is freshness. Fetching a hundred challenges and then solving them is the wrong shape, because the earliest ones expire while the batch works through. Fetch and solve inside the same task instead, one task per page, and let the gather handle the fan-out.
# pip install capskip
import asyncio
from capskip import AsyncCapSkip
async def solve_one(solver, page_url, challenge_url):
# One fresh challenge per page, fetched and solved together.
result = await solver.altcha(url=page_url, challenge_url=challenge_url)
return page_url, result["token"]
async def main():
solver = AsyncCapSkip()
pages = [
("https://example.com/signup", "https://example.com/altcha/challenge"),
("https://example.com/contact", "https://example.com/altcha/challenge"),
]
for page_url, token in await asyncio.gather(
*(solve_one(solver, p, c) for p, c in pages)
):
print(page_url, token[:24])
asyncio.run(main())Proof of work is CPU bound, so the ceiling is cores rather than open connections, and it is worth measuring on the machine you actually run rather than guessing. The general pattern for bulk solving, including the types where the wait is a browser session instead of hashing, is in the guide to solving CAPTCHAs in parallel.
Full working example
# pip install capskip
import os
import requests
from capskip import CapSkip, ApiException, NetworkException, TimeoutException
solver = CapSkip(host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"), port=8080)
try:
result = solver.altcha(
url="https://example.com/signup",
challenge_url="https://example.com/altcha/challenge",
)
# Submit here, while the challenge is still fresh.
r = requests.post("https://example.com/signup", data={
"email": "someone@example.com",
"altcha": result["token"],
})
print(r.status_code, "solved with counter", result["number"])
except ApiException as exc:
# ERROR_CAPTCHA_UNSOLVABLE here means Argon2id or scrypt.
print("refused:", exc)
except NetworkException:
print("CapSkip is not answering on that host and port")
except TimeoutException:
print("gave up waiting, which on this type means something is wrong")The other types are the same shape with a different method. There is one call for reCAPTCHA v2, v3 and Enterprise, one for Turnstile, one for GeeTest v3 and one for image CAPTCHAs, and the full list is on the Python CAPTCHA solver page.
The packages for Node.js, PHP and .NET expose the same method names. Every CapSkip SDK is described on the SDK page.
Common errors and what they mean
| What you see | Cause | Fix |
|---|---|---|
| A bare verification failure from the site, on a token that solved cleanly | The challenge expired before the form went in | Fetch, solve and submit inside one function |
| ERROR_CAPTCHA_UNSOLVABLE in an ApiException after a third of a second | The challenge asks for Argon2id or scrypt | Nothing to retry. Those two are refused on purpose |
| A ValidationException on the call | Neither challenge parameter was passed, or one that ALTCHA does not accept | Pass the challenge endpoint or the challenge document, and drop the rest |
| A NetworkException before any hashing happens | CapSkip is not running, or the host and port are wrong | Start CapSkip, then check whether it should be in Local mode or Server mode |
| A KeyError on the token key | Only an ALTCHA result carries that key | Call the altcha method, or read the code key, which holds the same string |
| The first solves in a batch fail and the last ones pass | Challenges were fetched up front and expired while queued | Fetch inside each task, not before the gather |
| A token the site rejects every time | Something stripped or re-encoded the payload | Pass the string through untouched |
FAQ
Do I need Selenium or Playwright for an ALTCHA page?
Not for the ALTCHA part. The challenge is a hashing problem and the answer is a string you post in a form field, so requests or httpx is enough and the solve finishes in milliseconds. You still want a browser if the rest of the page needs one, for instance when a session cookie is set by JavaScript or the form is rendered client side. In that case keep the browser for navigation and call the solver directly for the token, rather than trying to make the widget run.
Can a script on a hosted platform reach the solver?
Yes. Turn on Server mode under connection settings so CapSkip listens on a network address rather than loopback, then point CAPSKIP_HOST at it. A Docker container, a VPS, a scheduled job on a managed platform and a CI runner all connect the same way, over the same HTTP API. If the route crosses the internet, use a static public IP and a firewall rule that allows only the addresses you expect. The solver keeps running on your hardware either way, so the licence and the unlimited solving are unaffected.
Why is ALTCHA so much faster to solve than reCAPTCHA?
Because the two ask for different things. reCAPTCHA and Turnstile want evidence that a browser with a plausible history is present, which takes a real session and real time. ALTCHA only wants proof that some CPU was spent, so the work is a hash loop with a known stopping condition. That is also why the answer is deterministic rather than a judgement: there is a number that satisfies the challenge, and either it gets found or the challenge was broken. The tradeoff is that the number is worthless a minute or two later.
Can I reuse a token across requests?
No, and it is worth being explicit about why. The payload is signed against one specific challenge, that challenge is issued once, and the server tracks it, so a second submission of the same token is exactly the replay the design exists to stop. Solve once per submission. That is affordable here in a way it is not on a metered service, because the work is a few milliseconds of your own CPU rather than a billed call, so there is no reason to cache what you can simply recompute.
The short version
Read the challenge endpoint off the widget, pass it with the page URL to the one ALTCHA method, and post the token into the field named altcha without editing it. Keep the fetch, the solve and the submit together, because a challenge can die inside two minutes and an expired one is reported as a plain verification failure. For a batch, fan out with the async client and fetch each challenge inside its own task rather than collecting them first. Expect ERROR_CAPTCHA_UNSOLVABLE only from Argon2id and scrypt.
- How the challenge itself works: the ALTCHA solver page.
- Every other method the Python package exposes: the Python solver page.
One consequence is worth building into your retry logic. Running locally, a captcha solver does the hashing on a machine you already own, so a wasted solve costs milliseconds instead of credit. The right reaction to a stale challenge is therefore to fetch a new one and go again.
