hCaptcha in Python with the CapSkip SDK: One Call, One Token

hcaptcha in python - hCaptcha in Python with the CapSkip SDK: One Call, One Token

To solve hCaptcha in Python with the CapSkip SDK, call solver.hcaptcha() with the widget’s sitekey and the page address, then post your form with the token it returns and the User-Agent it reports. The SDK submits the task, polls for the result and hands both values back, so the script is mostly retry rules and the form post. The solver behind it is the CapSkip hCaptcha solver. It runs on your own Windows PC under a flat license, so replacing a token that expired or was refused costs nothing.

What you need

  • A Windows PC running CapSkip 4.0.0 or newer. That release, from 3 November 2026, added hCaptcha solving, Enterprise included. Install it by following the setup guide.
  • Python 3.10 or newer, which is the minimum the capskip package declares, and two packages: capskip 1.6.0 or newer, the first version with an hCaptcha method, and requests. Tested with capskip 1.6.0 and requests 2.34.2 on Python 3.11.7 and 3.14.0, and on no other versions.
  • A PowerShell prompt, where you run pip install capskip requests.
  • A page with an hCaptcha widget that you own or may automate, and its sitekey. Step 2 shows where to find it.
  • An address for CapSkip. Where the script runs decides the mode. Local mode (127.0.0.1) serves only the PC that CapSkip is on. Server mode also listens on your network or public IP, so another PC, a Windows VPS or a hosted platform can send the same requests. You switch modes in the connection settings. The code below expects the default CapSkip format, so leave any Service Emulator setting on its default.
  • Optional: a proxy. Step 2 covers where it goes.

How this was tested: against two mock servers that I wrote, not against the real CapSkip app or a live hCaptcha site. One mock copies the checks in the hCaptcha section of the CapSkip API docs. The other plays a sign-up form and doubles as a forward proxy. Nothing here measures a real solve time, and the site rules in the mock (single-use token, matching User-Agent, a two minute lifetime) come from the CapSkip docs, not from hCaptcha. The site you target does the real checking. For how that works, see the hCaptcha developer docs.

Step 1: create the solver

The four code blocks below belong in one file, in order. Name it hcaptcha_demo.py. Nothing runs until the last block is in place. The SDK in these runs was the real capskip 1.6.0, and only the CapSkip app behind it was a mock. The constructor’s apiKey argument defaults to the string capskip, which works while API Key Validation is off. If you turned validation on, pass your key as apiKey.

The constructor also sets how long the SDK waits for hCaptcha. Its recaptchaTimeout argument, 300 seconds by default, applies because the widget is passed in a real browser. The code lowers it to 180 seconds, which is my choice and not a CapSkip setting. That limit covers the polling only. The SDK sets no timeout on its own HTTP calls, and against the mock a reply held back for 20 seconds kept the call waiting the full 20 with recaptchaTimeout at 5. Problems arrive as exceptions that you import from capskip: ValidationException when the SDK refuses your arguments before sending anything, ApiException for a CapSkip error code, TimeoutException when the wait runs out and NetworkException when CapSkip can’t be reached.

# pip install capskip requests
import re

import requests
from capskip import ApiException, CapSkip

PAGE_URL = "https://example.com/signup"  # the page that shows the widget
SITEKEY = "YOUR_SITEKEY"  # the data-sitekey of its h-captcha div

# Local mode. In Server mode, use your server's address as the host.
# With API Key Validation on, also pass apiKey="YOUR_API_KEY".
solver = CapSkip(host="127.0.0.1", port=8080, recaptchaTimeout=180)

Step 2: ask CapSkip for the token

The sitekey is public and is the same for every visitor. Most pages print it on the widget container as data-sitekey. A page that builds the widget with a script passes it as the sitekey option of hcaptcha.render(), and the hCaptcha iframe address carries it as sitekey=. Put it in SITEKEY, and put the full address of the page that shows the widget in PAGE_URL.

The call is solver.hcaptcha(SITEKEY, PAGE_URL). It submits the task and polls until the token is ready, so you hold no task ID and write no loop. The raw API docs advise waiting 10 seconds first, but the SDK polls at once, then sleeps 0.25 seconds and doubles that after every not-ready answer, up to 5 seconds. Per the docs a solve takes 10 to 30 seconds as a rule. I never timed a real one.

The result is a dictionary. Its token and code fields hold the same string, the one for h-captcha-response, and userAgent is the User-Agent that earned it. The respKey field is a second value that hCaptcha hands the widget. It is always present, empty when there is none, and the docs say most sites never read it, so the code ignores it. Each result is delivered once and the SDK reads it for you, so the function hands back the token and the User-Agent together. The docs say CapSkip uses a userAgent argument only when it matches the browser it solves with, so the example lets CapSkip choose.

A proxy is optional for this method. The docs say one address is fine for testing, and that a steady stream of solves from a single IP is what hCaptcha’s rate limits are built to catch. For volume, load a proxy list in the hCaptcha settings or send a proxy with each task. A proxy on the task wins over the list. The function takes a proxy as a full URL such as http://user:[email protected]:8080 and sends the scheme as proxytype and the rest as the proxy field. HTTP and HTTPS proxies work as they are. For a SOCKS proxy, run pip install requests[socks] first. If you rotate proxies, the proxy rotation guide shows how.

def solve(proxy=None):
    options = {}
    if proxy:  # for example http://user:[email protected]:8080
        scheme, address = proxy.split("://", 1)
        # The SDK sends type as proxytype and uri as proxy.
        options["proxy"] = {"type": scheme.upper(), "uri": address}
    result = solver.hcaptcha(SITEKEY, PAGE_URL, **options)
    return result["token"], result["userAgent"]  # delivered once: keep both

Step 3: post the form with the same User-Agent

In a browser, the widget writes the token into a hidden textarea named h-captcha-response, with a copy named g-recaptcha-response. Without a browser you send h-captcha-response yourself, and g-recaptcha-response as well if the form has one. The code below sends both. Your form’s own fields come along with them, and their names are whatever the site uses. The post goes to PAGE_URL, so if the form’s action points to another address, change the first argument of the post call, PAGE_URL.

The User-Agent matters. The reply names the browser that earned the token, and the docs ask you to submit the form under that same string. So the function sets it on the session before the page is loaded, so the page load and the post carry the same User-Agent. Tokens are single use and expire about two minutes after they are issued, so post right away and ask for a new one on every attempt.

The post also names the page it came from as Referer, because requests sends neither a Referer nor an Origin header by itself. Some frameworks refuse an HTTPS form post that has neither. The Django CSRF documentation, for one, describes a strict referer check for HTTPS requests that come without an Origin header.

def submit_form(session, token, user_agent, email):
    # Every request from here on carries the User-Agent the token was
    # earned with, the page load included.
    session.headers["User-Agent"] = user_agent
    session.get(PAGE_URL, timeout=30)  # picks up the page's own cookies
    return session.post(
        PAGE_URL,
        data={
            "email": email,  # your form's own fields go here
            "h-captcha-response": token,
            "g-recaptcha-response": token,  # the widget fills both
        },
        headers={"Referer": PAGE_URL},  # requests sends no Referer by itself
        timeout=30,
    )

Step 4: the full example with retry rules

The last block ties the steps together into one script. Add it to the end of hcaptcha_demo.py, check that PAGE_URL and SITEKEY at the top are yours, and run python hcaptcha_demo.py from the folder that holds the file (py hcaptcha_demo.py works if python is not on your PATH). The script makes up to three attempts.

The error code decides what gets another try. The docs say ERROR_CAPTCHA_UNSOLVABLE means the task failed and a new one should be submitted, so it is the only code in the retry set. Two other failures name their cause because a resubmit would fail the same way: a sitekey that hCaptcha refused for the page and a proxy that can’t be reached. The other codes in the Common errors table point at the request or the setup. The SDK puts the code inside the ApiException message, as bare text or in a small JSON envelope, so error_code picks out the ERROR_ word and works with both. Everything else stops the script at once, including the three exceptions that the loop does not catch: ValidationException, TimeoutException and NetworkException.

A form that the site rejects gets a new token and another attempt. If three tokens fail the same way, look at the form fields. An HTTP 400 for a missing field repeats however good the token is. Also, reply.ok only means the status was below 400. Many sites answer 200 and show an error message in the page, so replace that test with a check for something your site shows only on success.

# Only a task CapSkip could not solve gets another try. A bad sitekey, a
# dead proxy or a wrong key would fail the same way again.
RETRY = {"ERROR_CAPTCHA_UNSOLVABLE"}


def error_code(err):
    # The message holds the code as bare text or inside a JSON envelope.
    found = re.search(r"ERROR_[A-Z_]+", str(err))
    return found.group(0) if found else str(err)


def sign_up(email, proxy=None):
    session = requests.Session()
    if proxy:
        session.proxies = {"http": proxy, "https": proxy}
    for attempt in range(1, 4):
        try:
            token, user_agent = solve(proxy)
        except ApiException as err:
            code = error_code(err)
            if code not in RETRY:
                raise
            print(f"attempt {attempt}: {code}, new task")
            continue
        reply = submit_form(session, token, user_agent, email)
        if reply.ok:
            return reply
        print(f"attempt {attempt}: the site answered {reply.status_code}, new token")
    raise RuntimeError("no accepted form after 3 attempts")


if __name__ == "__main__":
    reply = sign_up("[email protected]")
    print(reply.status_code, reply.text[:60])

In the mock run the script behaved like this, with the same results on Python 3.11.7 and 3.14.0:

Mock settingWhat the script did
Token ready on the second pollOne task, then the form was accepted with status 200
First two solves refusedTwo attempt lines, then status 200 on the third task
Site answers 403 to the first form postOne attempt line, a second task with a new token, then status 200
ERROR_PROXY_CONNECT_REFUSED on the pollRaised after one task, with no retry
Errors sent as bare text instead of JSONThe same behaviour as with JSON, including the retry
A proxy URL passed to sign_upin.php received the proxy with proxytype HTTP, and the page load and the form post both went through the proxy mock
Token posted under the default requests User-AgentThe mock site answered 403
The same token posted twiceThe second post got a 403
CapSkip never ready, recaptchaTimeout lowered to 25 secondsTimeoutException about 28 seconds after the call, because the last sleep between polls is 5 seconds

Treat the site rows as tests of my own mock. Its rules follow the CapSkip docs, except that I added a session cookie check and a requirement that both token fields match. They prove nothing about hCaptcha or about a real site.

Common errors

ErrorCauseFix
ValidationExceptionThe SDK refused the call before sending it: a blank sitekey, a page address that is not a full http or https URL, an invisible value other than 0, 1, true or false, an unsupported proxy type, or an option the CapSkip hCaptcha API does not take, such as 2Captcha’s domainRead the message, which names the problem, then correct the argument
ERROR_RECAPTCHA_INVALID_SITEKEYhCaptcha refused the sitekey for that page. An empty sitekey never gets this far, because the SDK refuses it firstCopy data-sitekey from the page again and check that PAGE_URL is the page that shows the widget
ERROR_CAPTCHA_UNSOLVABLEhCaptcha was not passedSubmit a new task. The loop does this up to three times
ERROR_PROXY_FORMATCapSkip could not read the proxy valueCheck the proxy value. CapSkip reads IP:PORT, LOGIN:PASSWORD@IP:PORT and IP:PORT:LOGIN:PASSWORD, and this script splits a URL such as http://user:pass@IP:PORT into a proxytype and the address after the scheme
ERROR_PROXY_CONNECT_REFUSEDThe target could not be reached through the proxy. I could only simulate itTest the proxy on its own, then recheck its type and login
ERROR_TASK_NOT_SUPPORTEDThis CapSkip does not accept the task type. Possible causes are a version older than 4.0.0, or a Service Emulator set to a service that refuses hCaptchaUpdate CapSkip to 4.0.0 or newer and leave the Service Emulator on its default
ERROR_WRONG_USER_KEY or ERROR_KEY_DOES_NOT_EXISTThe key is wrong while API Key Validation is on. The SDK sends the string capskip unless you pass an apiKeyPass your key as the apiKey argument of CapSkip()
TimeoutExceptionNo token after the 180 second wait that recaptchaTimeout sets in Step 1, or CapSkip stopped answering while the SDK was polling, which the SDK counts as not ready. It is not in the retry setOpen CapSkip, watch its Monitor for the task, then run the script again
NetworkExceptionNothing is listening on the host and port you gave CapSkip(), or CapSkip answered with an HTTP error statusStart CapSkip, then compare the host and port with the connection settings
The site refuses a token that looks validThe token was used before, was older than about two minutes, came with another User-Agent, or the post carried no Referer. The first three causes come from the CapSkip docsPost each token once, right away, under the User-Agent from the same reply, with the page as Referer

If the site still rejects the token

Some sites, hCaptcha Enterprise ones in particular, refuse tokens that hCaptcha scored as risky even though the widget was passed. The CapSkip docs call these strict sitekeys. The docs advise High score mode for hCaptcha, under Settings, Captcha types, hCaptcha. It needs Google Chrome installed on the PC that runs CapSkip. The changelog for 4.0.0 says the mode is on by default, and that hCaptcha solves in the CapSkip Custom Browser when it is off or Chrome is missing. The docs also ask for a good proxy, and for the form to be submitted from the same address under the User-Agent in the reply. I couldn’t test any of this without a real strict site, so treat it as the CapSkip docs’ guidance, not something I verified.

Using CapSkip on a server from your script

The only thing the script needs from CapSkip is a host and a port. In Server mode CapSkip also accepts connections from other devices on your network, or from the internet on a Windows VPS, and the setup guide recommends a static public IP for stable access. Server mode needs CapSkip to run as administrator. Pass the server’s address and port to CapSkip() and nothing else in the script changes. Ownership and billing don’t change either: the machine is yours and nothing is metered. That does leave the port open to other addresses, so create a Windows Firewall block rule for any you don’t trust.

The solve now happens on the server. For a strict sitekey, the docs ask for the same address on both sides, so give the proxy argument of sign_up a proxy and it goes to the task and to your session alike.

FAQ

Can I use the async client?

Yes. AsyncCapSkip takes the same constructor arguments, and awaiting its hcaptcha() method with the same sitekey and page address returns the same dictionary. It uses httpx, which pip installs with capskip. I ran it against the mock only.

Can I call CapSkip from a hosted platform?

Yes, through Server mode. Run CapSkip on a Windows machine with a reachable address, pass that address to CapSkip(), and the calls are the same. The solver stays on hardware you control, and the platform only sends HTTP requests to it.

What about invisible and Enterprise widgets?

They use the same hcaptcha() method. An invisible widget has the data-size attribute set to invisible, so pass invisible=True. For an Enterprise site that sets an rqdata value, pass it as data, and fetch a new one for every task. Against the mock, in.php received invisible=1 and the data value in both cases. I did not run either on a real site.

The short version

Solving hCaptcha in Python comes down to one hcaptcha() call and a form post under the User-Agent that earned the token. Retry only when CapSkip couldn’t solve the task, and let every other error stop the script. Because CapSkip is a local captcha solver on a flat license, retrying a refused token is only a matter of running the loop again. The Python CAPTCHA solver page has the SDK calls for the other types, and you can watch the widget pass in the hCaptcha demo.