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_answerThe 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 happened | How it looks | What to do |
|---|---|---|
| CapSkip did not locate the hole | Occasional, and the next attempt usually succeeds | Retry. Every attempt draws a brand new puzzle over a different photo |
| The Capy API refused the key | Every attempt, for that one key, and the Capy task list in CapSkip shows Invalid captcha key | Check 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:
raiseThe 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 see | Cause | Fix |
|---|---|---|
| The site rejects the submit although capy() returned all three values | The answer was altered, or only one or two of the values reached the form | Post result["answer"] verbatim, with the other two values, in one request |
| The site rejects the submit and says the session expired | The page was fetched with requests.get() and the form with a new request, so the page’s cookies never came back | Fetch the page and post the form through one requests.Session |
| A submit that worked once fails on the second attempt | The challenge key is single-use and short-lived | Solve again for every submission, and submit straight away |
| An ApiException on every attempt, for one key | The Capy API refused the key, or api_server points at the wrong host | Copy the key again, prefix included, and check the script tag’s host |
| An occasional ApiException containing ERROR_CAPTCHA_UNSOLVABLE | CapSkip could not locate the hole in that puzzle | Retry; the next attempt draws a different puzzle |
| Every solve fails after you copied a sample from another service | The sample sets api_server to api.capy.me, which no longer resolves | Remove the option and use the default host |
| A ValidationException before anything is sent | An 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 take | Make 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_EXIST | API key validation is on in CapSkip, and the script sent the default key or none | Set CAPSKIP_API_KEY to a key configured in CapSkip |
| A TimeoutException under heavy parallel load | More jobs queued than 10 threads can clear inside 120 seconds, or CapSkip restarted mid-solve | Cap concurrency with a semaphore, or raise defaultTimeout |
| A NetworkException on the first call | 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 |
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.
- How the type works and what the solver covers: the Capy Puzzle solver page.
- Every CAPTCHA type the Python package handles: the Python CAPTCHA solver page.
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.
