How to Solve Capy Puzzle in Python, From Key to Form Post

solve capy puzzle in python - How to Solve Capy Puzzle in Python, From Key to Form Post

To solve Capy Puzzle in Python, read the site’s PUZZLE_ key off the page, call solver.capy() from the capskip package with that key and the page URL, then post the three values it returns in the capy_captchakey, capy_challengekey and capy_answer form fields, together and straight away. Fetch the page and send the form through the same requests session, so the site’s cookies go back with the answer. The part that surprises people is the shape of the result. Most CAPTCHA types hand back one token; Capy hands back three values that only work as a set, and one of them expires quickly. CapSkip added Capy Puzzle in version 1.4.0. This guide covers the key, the call, the two second hold that is there on purpose, the submit, retries and running many solves at once.

What you need

  • CapSkip 1.4.0 or later running on a Windows machine. Capy support arrived in that release, together with CaptchaFox and Friendly Captcha.
  • Python 3.10 or later and version 1.3.0 or later of the capskip package, the first release with a capy() method. The samples also use requests, and the section on parallel solves uses httpx.
  • Two values from the target page: the Capy key, which starts with PUZZLE_, and the URL of the page the widget runs on. Step 1 shows where the key lives, and the same place tells you whether you need a third, optional value.
  • An address for the solver. In Local mode CapSkip answers on 127.0.0.1 for that device only; in Server mode it listens on your network address or public IP, so a script on another machine can call it over the API. Both are set under connection settings, and a section below covers when to switch.
# Quoted, so cmd.exe does not read >= as a redirect
pip install "capskip>=1.3.0" requests httpx

Step 1: find the PUZZLE_ key and the Capy host

Everything you need comes off the target page, and the key is public and identical for every visitor. Sites carry it in two places. The k parameter of the widget script’s URL is in the HTML that requests downloads. Once the widget has run in a browser, the key also sits in a hidden capy_captchakey input that the widget writes into the form, which is where DevTools shows it. One regular expression for the PUZZLE_ prefix finds it in either.

import re

import requests

PAGE_URL = "https://example.com/login"
session = requests.Session()
html = session.get(PAGE_URL, timeout=30).text

# The widget script's k= parameter. The capy_captchakey input only
# exists once the widget has run in a browser.
key = re.search(r"PUZZLE_[A-Za-z0-9_-]+", html)

# The widget script's own host; None means the default one.
host = re.search(r"(https://[^\"'\s<>]+?)/puzzle/get_js/", html)
api_server = host.group(1) if host else None
print(key and key.group(0), api_server)

While you are looking at the script tag, note its host. Everything in the script URL before /puzzle/get_js/ is the Capy API the key lives behind, and CapSkip calls it api_server. It defaults to https://jp.api.capy.me, where the live service runs, so you only need it when a page loads the widget from somewhere else. One trap comes from other solvers’ documentation: several still show api.capy.me without the regional prefix, and that host no longer resolves. If an old sample sets it, delete the option.

The fetch goes through a requests.Session on purpose. The form you submit in Step 3 often depends on a session cookie the page set, and a session object sends it back without any extra code.

Step 2: the capy() call

To solve Capy Puzzle in Python you need one method. It takes the key and the page URL, plus optional keyword arguments, and for a page on the default host those two values are all it needs.

# pip install "capskip>=1.3.0"
from capskip import CapSkip

solver = CapSkip(host="127.0.0.1", port=8080)

result = solver.capy("PUZZLE_YOUR_KEY", "https://example.com/login")

print(result["captchakey"])     # goes in capy_captchakey
print(result["challengekey"])   # goes in capy_challengekey
print(result["answer"])         # goes in capy_answer

The result is a plain dict. Read the three named keys. The code key holds the server’s whole answer object, a dict with all four fields, which is handy for logging, and respKey is an empty string that exists only for compatibility with other services. The answer is a long string that starts along the lines of 0xax8ex0xax84x: it is the drag path the widget would have recorded as the piece moved, not a coordinate.

Options the method accepts

The capy() method drops any keyword argument whose value is None before the request goes out, so you can pass the host from Step 1 on every call and let the regex decide whether it is sent.

result = solver.capy(
    key.group(0),
    PAGE_URL,
    # None (no get_js script on the page) is dropped, and CapSkip
    # then uses its default host.
    api_server=api_server,
    # Poll every half second; see the next section for why.
    polling_interval=0.5,
)

Besides api_server, capy() takes proxy, proxytype and useragent, plus a per-call timeout and polling_interval in seconds. The user agent, if you set one, is sent on the single request CapSkip makes to fetch the puzzle, and you will rarely need it. There is also a version option, but only puzzle is accepted. Capy’s other family, avatar, is a different challenge behind a different endpoint, so the SDK refuses it with a ValidationException rather than return an answer the site would reject. An unknown keyword, an empty key or page URL, or a proxy type other than HTTP, HTTPS, SOCKS5 or SOCKS5H raises the same exception before anything is sent.

Why a Capy solve takes about two seconds

The detection itself is fast. CapSkip fetches the puzzle image, finds the hole with some pixel math, and builds the drag path, with no browser and no model involved. Then it waits, deliberately.

Capy measures the time between drawing a puzzle and receiving the answer, and it refuses anything that arrives faster than a person could have dragged the piece. CapSkip’s own tests against Capy put that floor at about one second, and the refusal carries the same message as a wrong answer, so a correct solve delivered too quickly looks exactly like a broken solver. CapSkip therefore holds every Capy result until two seconds after it drew the puzzle. The wait is a sleep, so it costs latency and no CPU.

Why a call takes nearer four seconds with default settings: the SDK polls as soon as it has sent the job to CapSkip, then again after a quarter of a second, and keeps doubling the gap up to its pollingInterval, 5 seconds by default. The polls land at 0, 0.25, 0.75, 1.75 and 3.75 seconds, so a two second answer is collected at the fifth poll. With polling_interval=0.5 on the call, or pollingInterval=0.5 in the constructor if Capy is most of what the client solves, the call returns a little over two seconds after the job was sent, at about 2.3 in our tests. Do not add a delay of your own before you post the form, and do not try to shave the hold off: it is exactly what makes the answer pass.

Step 3: submit all three values in one request

The three values go into the fields the widget would have written into the form itself. Send them together, in the same request as the rest of the form, through the session that fetched the page.

# session is the one Step 1 used, so the page's cookies go back too.
resp = session.post(
    PAGE_URL,  # or wherever the form's action attribute points
    data={
        "username": "YOUR_USERNAME",
        "capy_captchakey": result["captchakey"],
        "capy_challengekey": result["challengekey"],
        "capy_answer": result["answer"],
    },
    timeout=30,
)
print(resp.status_code)

Submit the answer exactly as returned. The site’s backend checks it against the puzzle that was drawn, so trimming it, rebuilding it or tidying it in any way invalidates it. The requests library percent-encodes the form body, which is fine, because the server decodes it before checking. Then submit promptly. CapSkip generates a fresh challenge key for every solve and the puzzle is bound to it, so the key is single-use and short-lived, and one solve covers one submission.

Mirror what the real form sends. Most pages carry hidden fields of their own, such as an anti-forgery token, and some submit through script with a JSON body instead of a form post. Submit once by hand with DevTools open, copy the request, and put the three Capy values where the page puts them.

Step 4: retries when a solve fails

When you solve Capy Puzzle in Python at any volume, a few solves will fail. A failed solve arrives as an ApiException whose message contains ERROR_CAPTCHA_UNSOLVABLE, and through res.php, the endpoint the SDK polls, that one code covers two different situations. What tells them apart is how often they happen.

What happenedHow it looksWhat to do
CapSkip did not locate the holeOccasional, and the next attempt usually succeedsRetry. Every attempt draws a brand new puzzle over a different photo
The Capy API refused the keyEvery attempt, for that one key, and the Capy task list in CapSkip shows Invalid captcha keyCheck the key and api_server. CapSkip never retries a refused key, because it would be refused the same way

Misses are rare, so one retry covers nearly everything. Set Retries in the Capy section of CapSkip’s settings, which is 0 by default and allows up to three per task, or retry in your own code:

from capskip import ApiException


def solve_capy(key, url, attempts=3, **options):
    for attempt in range(1, attempts + 1):
        try:
            return solver.capy(key, url, **options)
        except ApiException as exc:
            # A missed hole reads as unsolvable, and the next try
            # draws a new puzzle. Anything else is final.
            if "UNSOLVABLE" not in str(exc) or attempt == attempts:
                raise

The capy() method polls on the client’s defaultTimeout, 120 seconds, because a Capy solve is one fetch and some arithmetic rather than a browser session. CapSkip runs its own clock as well: in the Capy section, a task can wait up to 250 seconds (Wait Timeout) for one of the 10 threads (Max. Threads), and one attempt gets 60 seconds (Row Timeout). A normal solve fits inside all of that many times over.

Solving many Capy puzzles at once

AsyncCapSkip is a genuine asyncio client in Python, not an alias, so asyncio.gather runs several solves side by side. Keep every solve paired with its own submit. Gathering a hundred solves first and posting afterwards leaves the earliest challenge keys ageing while the last solves finish.

import asyncio

import httpx
from capskip import AsyncCapSkip

solver = AsyncCapSkip(host="127.0.0.1", port=8080, pollingInterval=0.5)
slots = asyncio.Semaphore(10)  # CapSkip's Capy Max. Threads


async def solve_and_submit(key, url, form):
    # One client per job, so each form session keeps its own cookies.
    async with slots, httpx.AsyncClient(
            timeout=30, follow_redirects=True) as client:
        await client.get(url)
        result = await solver.capy(key, url)
        return await client.post(url, data={
            **form,
            "capy_captchakey": result["captchakey"],
            "capy_challengekey": result["challengekey"],
            "capy_answer": result["answer"],
        })


async def main(jobs):
    return await asyncio.gather(
        *(solve_and_submit(*job) for job in jobs), return_exceptions=True)

The semaphore matches CapSkip’s thread count, so no job sits in CapSkip’s queue holding a session open, and it comes from the Python standard library, documented under asyncio.Semaphore. At volume, add proxies too. Every solve draws a new puzzle from the Capy API, and a steady stream of those from one address is the pattern rate limiting exists to catch. Configure a proxy pool in CapSkip’s Capy section, or pass a proxy dict with a type and a uri on each call; it only touches the puzzle fetch, the one request a Capy solve makes. More asyncio patterns are in the guide to solving CAPTCHAs in parallel in Python.

Running the solver on another machine

127.0.0.1 is right while the script and CapSkip share a Windows PC. When the Python code moves to a VPS, a container, a CI runner or a hosted notebook, loopback points at the wrong machine and the first solve raises a NetworkException. Switch CapSkip to Server mode and it listens on your network address or public IP, so any of those can call it over the same API. Use a static public IP when the route crosses the internet, turn on API key validation, and limit the port to the addresses you expect with a Windows Firewall rule. The solver is still your own Windows machine, and solves are still unmetered.

The SDK does not read environment variables by itself. Read CAPSKIP_HOST, CAPSKIP_PORT and CAPSKIP_API_KEY in your code and pass them to the constructor, as the full example does, so the same script runs at your desk and on a server.

Full working example

# pip install "capskip>=1.3.0" requests
import os
import re

import requests
from capskip import (ApiException, CapSkip, CapSkipError,
                     TimeoutException, ValidationException)

PAGE_URL = "https://example.com/login"

solver = CapSkip(
    apiKey=os.getenv("CAPSKIP_API_KEY", "capskip"),
    host=os.getenv("CAPSKIP_HOST", "127.0.0.1"),
    port=int(os.getenv("CAPSKIP_PORT", "8080")),
    # Capy answers after a two second hold; poll often enough to catch it.
    pollingInterval=0.5,
)
session = requests.Session()

html = session.get(PAGE_URL, timeout=30).text
key = re.search(r"PUZZLE_[A-Za-z0-9_-]+", html)
if not key:
    raise SystemExit("No PUZZLE_ key in the HTML; find it in DevTools.")
host = re.search(r"(https://[^\"'\s<>]+?)/puzzle/get_js/", html)

try:
    for attempt in range(1, 4):
        try:
            result = solver.capy(key.group(0), PAGE_URL,
                                 api_server=host.group(1) if host else None)
            break
        except ApiException as exc:
            # A missed hole reads as unsolvable; the next try draws a new puzzle.
            if "UNSOLVABLE" not in str(exc) or attempt == 3:
                raise
            print(f"attempt {attempt}: {exc}")
except ValidationException as exc:
    raise SystemExit(f"not sent: {exc}")
except TimeoutException:
    raise SystemExit("gave up waiting; defaultTimeout is 120 seconds")
except CapSkipError as exc:
    # A third miss, a refused key, or CapSkip unreachable.
    raise SystemExit(f"solve failed: {exc!r}")

# All three together, straight away: the challenge key is single-use.
resp = session.post(
    PAGE_URL,  # or wherever the form's action attribute points
    data={
        "username": "YOUR_USERNAME",
        "capy_captchakey": result["captchakey"],
        "capy_challengekey": result["challengekey"],
        "capy_answer": result["answer"],
    },
    timeout=30,
)
print(resp.status_code)

If all three attempts fail, look at the key before anything else, since a refused key fails the same way every time while a miss almost never happens three times running. When the page builds the widget from a bundled script and the regex finds nothing, open the Network tab and copy the key and host from the widget’s own request. The raw endpoint behind this method is documented in the Capy section of the API reference.

Common errors and what they mean

What you seeCauseFix
The site rejects the submit although capy() returned all three valuesThe answer was altered, or only one or two of the values reached the formPost result["answer"] verbatim, with the other two values, in one request
The site rejects the submit and says the session expiredThe page was fetched with requests.get() and the form with a new request, so the page’s cookies never came backFetch the page and post the form through one requests.Session
A submit that worked once fails on the second attemptThe challenge key is single-use and short-livedSolve again for every submission, and submit straight away
An ApiException on every attempt, for one keyThe Capy API refused the key, or api_server points at the wrong hostCopy the key again, prefix included, and check the script tag’s host
An occasional ApiException containing ERROR_CAPTCHA_UNSOLVABLECapSkip could not locate the hole in that puzzleRetry; the next attempt draws a different puzzle
Every solve fails after you copied a sample from another serviceThe sample sets api_server to api.capy.me, which no longer resolvesRemove the option and use the default host
A ValidationException before anything is sentAn empty key or page URL, avatar as the version, a proxy type other than HTTP, HTTPS, SOCKS5 or SOCKS5H, or a keyword the method does not takeMake sure the key is not an empty string, and fix or drop the argument the message names
An ApiException with ERROR_WRONG_USER_KEY or ERROR_KEY_DOES_NOT_EXISTAPI key validation is on in CapSkip, and the script sent the default key or noneSet CAPSKIP_API_KEY to a key configured in CapSkip
A TimeoutException under heavy parallel loadMore jobs queued than 10 threads can clear inside 120 seconds, or CapSkip restarted mid-solveCap concurrency with a semaphore, or raise defaultTimeout
A NetworkException on the first callCapSkip is not running, or the host and port are wrongStart CapSkip, then check whether it should be in Local mode or Server mode

FAQ

Why does capy() return three values instead of a token?

Because that is what the Capy form posts. No server issues a token at any point. The widget itself generates a challenge key, fetches the puzzle that belongs to it and records the drag, and the site then sends the challenge key and the answer to Capy with its private key to have them checked. CapSkip plays the part of the widget, so it returns what the widget would have written into the form.

Does the python-requests user agent get the answer refused?

Not by Capy. A Capy answer is not tied to a browser, CapSkip returns no user agent for it, and the optional one you can pass only changes the request that fetches the puzzle. The site itself may still turn away a request that announces itself as python-requests, for reasons of its own. If it does, set a browser User-Agent on the session once, and every request from it, including the submit, carries that header.

Can I use the answer in a Selenium or Playwright session?

Yes. Solve with the page URL the browser has open, then write the three values into the form’s capy_captchakey, capy_challengekey and capy_answer hidden inputs with a short script, adding any input the widget has not created yet, and submit the form as the page normally would. Do not drag the piece in the browser as well, since that would produce a second, different answer. If the browser is there only to get past the puzzle, the requests route above is simpler and faster.

Can a Python script on a hosted platform reach the solver?

Yes. Put CapSkip in Server mode under connection settings so it listens on a network address instead of loopback, read that address from CAPSKIP_HOST in your script, and pass it to CapSkip(). A VPS, a container host, a CI runner and a hosted notebook all connect over the same HTTP API. Use a static public IP and a firewall rule when the route crosses the internet. The solver stays on hardware you own, so nothing changes about how solves are counted. The same flow in .NET is covered in the C# Capy Puzzle guide.

The short version

To solve Capy Puzzle in Python, fetch the page with a requests session, pull the PUZZLE_ key out with one regex, and note the widget script’s host in case it is not the default. Call solver.capy() with the key and the real page URL and let it take its few seconds, about 2.3 with polling_interval=0.5. Post captchakey, challengekey and answer in the three capy_ fields, verbatim, together and at once, through the same session. Retry the occasional miss, add proxies as volume grows, and switch to Server mode when the script leaves the solver’s machine.

One last point about that retry. Every attempt draws a different puzzle, so trying again is the right fix for a miss, and with an unlimited captcha solver on your own machine a second attempt costs a couple of seconds rather than another billed solve.