Complete reference aligned with the official CapSkip API.
CapSkip exposes a standard captcha-solver HTTP API on your local machine:
POST http://<host>:<port>/in.php → submit captcha
GET http://<host>:<port>/res.php → poll result
The SDK only supports the captcha types documented by CapSkip.
| Type | SDK method | method (POST) |
|---|---|---|
| Image captcha | normal() |
post or base64 |
| reCAPTCHA v2 | recaptcha(..., version='v2') |
userrecaptcha |
| reCAPTCHA v3 | recaptcha(..., version='v3') |
userrecaptcha + version=v3 |
| Cloudflare Turnstile | turnstile() |
turnstile |
| GeeTest v3 (slide) | geetest() |
geetest |
| ALTCHA (proof-of-work) | altcha() |
altcha |
| Capy Puzzle (slide) | capy() |
capy |
| CaptchaFox (widget) | captchafox() |
captchafox |
| Friendly Captcha (proof-of-work) | friendly_captcha() |
friendly_captcha |
| AWS WAF CAPTCHA (Amazon WAF) | amazon_waf() |
amazon_waf |
| DataDome (slider and device check) | datadome() |
datadome |
| hCaptcha (checkbox, invisible, Enterprise) | hcaptcha() |
hcaptcha |
Proxy is supported for every method except image captcha. For ALTCHA the
proxy is used only for the challenge_url fetch; for Capy, only for the
puzzle-image fetch, which is the only request a Capy solve makes.
from capskip import CapSkip
solver = CapSkip(
apiKey="capskip",
host="127.0.0.1",
port=8080,
defaultTimeout=120,
recaptchaTimeout=300,
pollingInterval=5, # max seconds between polls; starts at 0.25s and backs off to this
)| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | CapSkip API key |
method |
string | Yes | post (multipart file) or base64 |
file |
file | Yes* | Image file when method=post |
body |
string | Yes* | Base64 image when method=base64 |
json |
int | No | 0 plain text (default), 1 JSON |
| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | CapSkip API key |
action |
string | Yes | get |
id |
int | Yes | Captcha ID from in.php |
json |
int | No | 0 plain text (default), 1 JSON |
result = solver.normal("captcha.png")
result = solver.normal("https://example.com/captcha.jpg")
result = solver.normal("data:image/png;base64,iVBORw0KGgo...", json=1)
print(result["code"])Only json is accepted as an extra parameter. Proxy is not supported.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
key |
string | Yes | — | CapSkip API key |
method |
string | Yes | — | userrecaptcha |
googlekey |
string | Yes | — | Site key (data-sitekey / k) |
pageurl |
string | Yes | — | Full page URL |
enterprise |
int | No | 0 |
1 = Enterprise v2 |
invisible |
int | No | 0 |
1 = Invisible reCAPTCHA |
data-s |
string | No | — | Google Search / services data-s value |
json |
int | No | 0 |
1 = JSON response |
proxy |
string | No | — | IP:PORT or login:pass@IP:PORT |
proxytype |
string | No | HTTP |
HTTP, HTTPS, SOCKS5, SOCKS5H |
Do not send version, action, or min_score for v2.
Same as image captcha poll parameters.
# Standard v2
result = solver.recaptcha(sitekey="...", url="https://example.com")
# Invisible v2
result = solver.recaptcha(sitekey="...", url="...", invisible=1)
# Enterprise v2
result = solver.recaptcha(sitekey="...", url="...", enterprise=1)
# Enterprise v2 with data-s (SDK alias: datas=...)
result = solver.recaptcha(sitekey="...", url="...", enterprise=1, datas="...")
# With proxy
result = solver.recaptcha(
sitekey="...",
url="...",
proxy={"type": "HTTPS", "uri": "user:pass@1.2.3.4:3128"},
)| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
key |
string | Yes | — | CapSkip API key |
method |
string | Yes | — | userrecaptcha |
version |
string | Yes | — | v3 |
googlekey |
string | Yes | — | Site key |
pageurl |
string | Yes | — | Full page URL |
enterprise |
int | No | 0 |
1 = Enterprise v3 |
action |
string | No | verify |
Action from grecaptcha.execute() |
min_score |
float | No | 0.4 |
Minimum acceptable score |
json |
int | No | 0 |
1 = JSON response |
proxy |
string | No | — | Proxy address |
proxytype |
string | No | HTTP |
Proxy type |
Do not send invisible for v3.
result = solver.recaptcha(
sitekey="...",
url="https://example.com",
version="v3",
action="submit",
min_score=0.7, # or SDK alias: score=0.7
enterprise=0,
)| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | CapSkip API key |
method |
string | Yes | turnstile |
sitekey |
string | Yes | Turnstile sitekey |
pageurl |
string | Yes | Full page URL |
action |
string | No | From data-action or turnstile.render() |
data |
string | No | cData / data-cdata |
pagedata |
string | No | chlPageData (challenge pages) |
json |
int | No | 0 plain text, 1 JSON |
proxy |
string | No | Proxy address |
proxytype |
string | No | Proxy type |
| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | CapSkip API key |
action |
string | Yes | get |
id |
int | Yes | Captcha ID |
json |
int | Yes | Must be 1 to receive User-Agent |
The SDK automatically polls Turnstile results with json=1 and includes userAgent in the result when CapSkip returns it.
# Standalone widget
result = solver.turnstile(sitekey="0x4AAAAAAA...", url="https://example.com")
print(result["code"])
print(result.get("userAgent")) # present when CapSkip returns it
# Challenge page
result = solver.turnstile(
sitekey="0x4AAAAAAA...",
url="https://example.com",
action="managed",
data="cData_value",
pagedata="chlPageData_value",
)
# Use result["userAgent"] when submitting the token| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | CapSkip API key |
method |
string | Yes | geetest |
gt |
string | Yes | Static per-site GeeTest id |
challenge |
string | Yes | Single-use challenge token |
pageurl |
string | Yes | Full page URL |
api_server |
string | No | GeeTest API server domain, e.g. api-na.geetest.com |
json |
int | No | 0 plain text, 1 JSON |
proxy |
string | No | Proxy address |
proxytype |
string | No | Proxy type |
Both come from the target site, which fetches them from an endpoint returning
{"gt": "...", "challenge": "..."} (often .../register.php or a gettype/get.php
request). Find it in DevTools → Network, or read them out of the
initGeetest({ gt, challenge }) call in the page scripts.
challengeis single-use and expires in about a minute. Fetch a fresh pair immediately before each solve. If a solve comes back with a bad-challenge error, request a new pair and retry — reusing one never succeeds.
result = solver.geetest(
gt="81388ea1fc187e0c335c0a8907ff2625",
challenge="7cf6a8b1a2c34d5e6f7089abcdef0123",
url="https://example.com/login",
)
result["challenge"] # geetest_challenge
result["validate"] # geetest_validate
result["seccode"] # geetest_seccode
result["code"] # the same answer as a raw JSON stringPost the three fields back exactly as the site's own front-end would:
requests.post(LOGIN_URL, data={
"geetest_challenge": result["challenge"],
"geetest_validate": result["validate"],
"geetest_seccode": result["seccode"],
})GeeTest is a real browser solve, so it uses the longer recaptchaTimeout budget
rather than defaultTimeout.
ALTCHA is not a recognition captcha. There is no image, audio or text to read: the site issues a proof-of-work challenge and the client must brute-force a number that satisfies it. A solve is therefore deterministic and cheap — typically milliseconds.
| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | CapSkip API key |
method |
string | Yes | altcha |
pageurl |
string | Yes | Full URL of the page the challenge came from |
challenge_url |
string | One of the two | Endpoint CapSkip fetches the challenge from |
challenge_json |
string | One of the two | The challenge document itself, as a JSON string |
json |
int | No | 0 plain text, 1 JSON |
proxy |
string | No | Proxy address — used only for the challenge_url fetch |
proxytype |
string | No | Proxy type |
Sending both challenge parameters is allowed: the inline challenge_json wins,
because fetching would only re-obtain what you already supplied.
Open DevTools → Network on the target page and look for the request the
<altcha-widget> makes for its challenge (often something like
/altcha/challenge). The request URL is your challenge_url; its JSON response
is your challenge_json.
The widget attribute that names that endpoint depends on the widget version, so read the page source rather than assuming:
| Widget | Attribute |
|---|---|
| v1 / v2 | challengeurl="…", with a separate challengejson="…" for an inline challenge |
| v3+ | challenge="…" — the same attribute takes either a URL or the challenge data |
Challenges expire, and the window is short — some sites inside two minutes. Once expired, the site refuses the solution with a bare "verification failed" that looks exactly like a wrong answer. Fetch the challenge immediately before solving and submit the token promptly; do not fetch a batch in advance, and do not hold a token while a user fills in a form.
CapSkip refuses an already-expired inline challenge immediately rather than burning CPU on a token that cannot work. If you passed
challenge_urland the challenge expired while the job queued, it fetches a fresh one automatically.
# CapSkip fetches the challenge for you
result = solver.altcha(
url="https://example.com/signup",
challenge_url="https://example.com/captcha/api/altcha/challenge",
)
# …or hand it the document you already have. No network request at all.
result = solver.altcha(
url="https://example.com/signup",
challenge_json={"algorithm": "SHA-256", "challenge": "…", "salt": "…",
"signature": "…", "maxnumber": 1000000},
)
result["token"] # the base64 payload to post back
result["number"] # the counter that solved it
result["code"] # the same string as tokenchallenge_json accepts a dict (serialized for you) or a JSON string.
number is reported for both ALTCHA generations. Their tokens differ — a legacy
payload carries it as a top-level number, while a proof-of-work v2 payload has
none, its counter sitting at solution.counter — so it is read from the server's
own solution object, which reports both the same way.
Post the token back in the form field the widget uses, named altcha:
requests.post(SIGNUP_URL, data={
"email": "someone@example.com",
"altcha": result["token"],
})Do not re-encode, trim or re-order the token: it is base64 of a JSON document whose fields are covered by the server's HMAC signature, so any modification invalidates it. Some integrations read the payload from a JSON body field instead — check what the page's own submit sends and mirror it.
Unlike GeeTest and reCAPTCHA this is CPU proof-of-work rather than a browser
solve, so it uses defaultTimeout, not recaptchaTimeout.
CapSkip supports the legacy scheme (SHA-1/256/384/512) and PoW v2 with PBKDF2 or
SHA. Argon2id and scrypt are refused, not attempted: a task using one returns
ERROR_CAPTCHA_UNSOLVABLE and is never retried. ALTCHA itself recommends PBKDF2
as the default, so this affects a minority of sites.
All three widget types (native, checkbox, switch) work — the distinction is
purely visual and never reaches CapSkip.
Capy is a slide puzzle: a piece has to be dragged into the hole cut out of a photograph. CapSkip locates the hole and produces the drag path — one HTTP fetch plus pixel math, no browser.
| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | CapSkip API key |
method |
string | Yes | capy |
captchakey |
string | Yes | The site's public Capy key, conventionally prefixed PUZZLE_. The SDK sends its sitekey argument here. |
pageurl |
string | Yes | Full URL of the page the captcha is on |
api_server |
string | No | Root of the Capy API the key lives behind. Default https://jp.api.capy.me |
version |
string | No | puzzle (the default). Only the puzzle family is solved |
useragent |
string | No | User-Agent to send with the puzzle request |
json |
int | No | 0 plain text, 1 JSON |
proxy |
string | No | Proxy address |
proxytype |
string | No | Proxy type |
sitekey and websiteKey are accepted by the server as aliases for
captchakey; the SDK sends the documented name.
version="avatar" is refused by the SDK before the request is made. Avatar is a
different challenge behind a different endpoint, and answering it with a puzzle
answer would bill for a solve the target site rejects, indistinguishably from a
broken solver.
api.capy.meno longer resolves, although several solver services still document it. The live host isjp.api.capy.me; if a site's widget points somewhere else, pass that asapi_server.
It is in the page source as capy_captchakey, or in the widget script URL:
<script src="https://jp.api.capy.me/puzzle/get_js/?k=PUZZLE_XXXX">.
The answer is not a token. It is an object, which the SDK expands into three result fields. See Return value.
result = solver.capy(
"PUZZLE_Abc1dEFghIJKLM2no34P56q7rStu8v",
"https://example.com/login",
)
# With a non-default API server
result = solver.capy(
"PUZZLE_Abc1dEFghIJKLM2no34P56q7rStu8v",
"https://example.com/login",
api_server="https://jp.api.capy.me/",
)
# capy_captchakey, capy_challengekey, capy_answer
result["captchakey"], result["challengekey"], result["answer"]Capy grades the wall-clock gap between issuing the puzzle and verifying the
answer, and refuses anything superhuman with CAPTCHA verification failed — the
same message a wrong answer gets. CapSkip therefore holds every result until two
seconds have elapsed since it drew the puzzle. This is invisible if you poll: the
task simply takes about two seconds instead of a fifth of one. Nothing to
configure.
The challenge key is single-use and short-lived, so submit the three values promptly rather than caching them.
CaptchaFox scores the browser itself rather than asking the visitor to read anything; most visitors never see a puzzle. CapSkip drives the real widget in a real browser and returns the verification token.
| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | CapSkip API key |
method |
string | Yes | captchafox |
sitekey |
string | Yes | The public key the widget renders with, conventionally prefixed sk_ |
pageurl |
string | Yes | Full URL of the page the widget appears on |
api_server |
string | No | Widget entry point. Default https://cdn.captchafox.com/ |
useragent |
string | No | Accepted for compatibility and not applied — CapSkip uses its own browser's identity |
json |
int | No | 0 plain text, 1 JSON |
proxy |
string | No | Proxy address |
proxytype |
string | No | Proxy type |
api_server |
Token | Default |
|---|---|---|
https://cdn.captchafox.com/ |
Plain | Yes |
https://s.uicdn.com/mampkg/… |
MAM_ prefixed |
No |
Read the value from the <script> tag that loads the widget. If you send the
wrong one the solve still succeeds, but the token comes back in a format the
target site will not accept — a silent verification failure rather than an error.
CaptchaFox keys are registered against a list of allowed domains and the service checks the host before issuing anything. A correct key used on a page outside that list is refused permanently, not intermittently. CapSkip reports that case rather than retrying it, because retrying cannot help.
| Challenge | Frequency | Supported |
|---|---|---|
| Invisible | Usually | Yes |
| Slide | Sometimes | Yes |
| Image select | Rarely | No |
| Audio | Rarely | No |
The two unsupported ones are uncommon and a retry usually draws a different challenge, so treat an unsolvable result as a signal to resubmit rather than a permanent failure of the key.
result = solver.captchafox(
"sk_xtNxpk6fCdFbxh1_xJeGflSdCE9tn99G",
"https://example.com/signup",
)
# Post in the form field named cf-captcha-response
result["token"]
# Submit under the UA that minted the token, not your own
result.get("userAgent")Uses recaptchaTimeout — it is a real browser session, and longer again when an
interactive challenge is drawn.
Proof-of-work, with nothing shown on screen. The widget searches for values that hash below a difficulty the service sets per request.
| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | CapSkip API key |
method |
string | Yes | friendly_captcha |
sitekey |
string | Yes | The data-sitekey of the element carrying class="frc-captcha" |
pageurl |
string | Yes | Full URL of the page the widget appears on |
version |
string | No | v1 (default) or v2; a bare 1 or 2 is accepted |
module_script |
string | No | src of the widget script tag carrying type="module" |
nomodule_script |
string | No | src of the widget script tag carrying nomodule |
api_server |
string | No | Data residency endpoint: global (default), eu, or a full URL |
useragent |
string | No | User-Agent to send with the request |
json |
int | No | 0 plain text, 1 JSON |
proxy |
string | No | Proxy address |
proxytype |
string | No | Proxy type |
Two entirely different protocols ship under this one name, and a sitekey does not tell you which one a site uses — they share a brand and a sitekey namespace and nothing else. Both are live. Solve the wrong one and you get a well-formed token the target site rejects, with no indication that the version was the problem.
version |
Widget package | Script |
|---|---|---|
v1 |
friendly-challenge |
widget.module.min.js / widget.min.js |
v2 |
@friendlycaptcha/sdk |
site.min.js |
CapSkip decides in this order, stopping at the first answer: the version
parameter; then the script URL from module_script or nomodule_script, which
is the most reliable signal because it is the build the site actually loads; then
v1. The SDK refuses any other version value locally.
version |
Form field |
|---|---|
v1 |
frc-captcha-solution |
v2 |
frc-captcha-response |
The field names differ, which is what catches an integration moved from one to
the other. A v1 token is four dot-separated parts and runs to a few hundred
characters; a v2 token is a single opaque string beginning AQQA. and is roughly
six kilobytes, so size whatever carries it accordingly.
# Say which version
result = solver.friendly_captcha(
"FCMGEMUD2M567T8G",
"https://example.com/signup",
version="v2",
)
# …or let the script URL decide it
result = solver.friendly_captcha(
"FCMGEMUD2M567T8G",
"https://example.com/signup",
module_script="https://cdn.example.com/@friendlycaptcha/sdk@0.1.6/site.min.js",
)
# EU data-residency tenant
result = solver.friendly_captcha(
"FCMGEMUD2M567T8G",
"https://example.com/signup",
version="v2",
api_server="eu",
)
result["token"]Uses recaptchaTimeout: the service decides how much work a request is worth at
the moment it is made, so solve time is not a constant, and v2 always solves in a
browser.
AWS WAF CAPTCHA is the puzzle Amazon Web Services shows when a site's firewall wants proof that a visitor is human: usually a 3 x 3 grid of pictures, sometimes a toy car to steer along a path. AWS WAF decides which; you do not have to say. Public docs: https://capskip.com/api-docs/#aws-waf.
The arguments follow 2Captcha's amazon_waf(sitekey, iv, context, url), so code
written against it keeps working, but only sitekey and url are required.
| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | CapSkip API key |
method |
string | Yes | amazon_waf |
sitekey |
string | Yes | gokuProps.key of a challenge page, or the widget's apiKey |
pageurl |
string | Yes | Full http or https URL of the protected page |
iv |
string | No | gokuProps.iv. Send together with context, or leave both out |
context |
string | No | gokuProps.context. Send together with iv, or leave both out |
challenge_script |
string | No | Full URL of the page's challenge.js |
captcha_script |
string | No | Full URL of the page's captcha.js |
jsapiScript |
string | No | Widget pages only: full URL of jsapi.js |
json |
int | No | 0 plain text, 1 JSON |
proxy |
string | No | Proxy address |
proxytype |
string | No | Proxy type |
Refused by the SDK before the request is made, as the server would refuse them:
a missing sitekey or pageurl, a pageurl that is not http(s), an iv
without context (or the other way round), and any script URL that is not
https on an awswaf.com host ending in challenge.js, captcha.js or jsapi.js.
- Challenge page. The firewall answers with HTTP 405 and a small page
carrying
window.gokuPropsand loading challenge.js and captcha.js from an awswaf.com host. SendgokuProps.keyassitekey, plusivandcontext, and optionally the two script URLs. Any of those you leave out is read frompageurlat solve time, which is the safer choice when your copy of the page is more than a few minutes old. - Widget page. An ordinary page loads jsapi.js and draws the puzzle with
AwsWafCaptcha.renderCaptcha. Send itsapiKeyassitekeyand the jsapi.js URL asjsapiScript, with noivorcontext.
The answer is two values, so like Capy's it is an object. The SDK expands it
into captcha_voucher and existing_token. See Return value.
# Challenge page, 2Captcha's argument order
result = solver.amazon_waf(sitekey, iv, context, "https://www.example.com/",
challenge_script=challenge_js)
# Let CapSkip read iv and context off the page itself
result = solver.amazon_waf(sitekey, url="https://www.example.com/")
# Widget page
result = solver.amazon_waf(
sitekey=api_key,
url="https://www.example.com/contact",
jsapiScript="https://5b7e0a19c3d8.us-east-1.captcha-sdk.awswaf.com/5b7e0a19c3d8/jsapi.js",
)
result["captcha_voucher"], result["existing_token"]The pair only works together. POST it as JSON to voucher in the same folder as
the page's challenge.js (on a widget page, jsapi.js loads a challenge.js for
you; find it in the Network tab). The response's token field is the
aws-waf-token: send it as that cookie on the protected domain, or in an
x-aws-waf-token header if that is what the page's own requests do. Exchange
the pair as soon as it arrives rather than storing it for later.
Uses recaptchaTimeout: a real browser mints the token the voucher is issued
for, and the puzzle rounds go through AWS on top of that.
DataDome is the bot protection behind many large shops, news sites and
marketplaces. When it doubts a visitor, the site answers with a block page
(status 403, sometimes 401) holding a slider or a short device check. CapSkip
passes that challenge and returns the datadome cookie; requests that carry it
are let through. Public docs: https://capskip.com/api-docs/#datadome.
The arguments follow 2Captcha's datadome(captcha_url, pageurl, userAgent, proxy), so code written against it keeps working; userAgent and proxy are
optional here. url is accepted for pageurl, as on every other method.
| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | CapSkip API key |
method |
string | Yes | datadome |
captcha_url |
string | Yes | Address of the challenge iframe on geo.captcha-delivery.com |
pageurl |
string | Yes | Full http or https URL of the page that returned the block page |
userAgent |
string | No | The User-Agent you will send with the cookie: Chrome for Windows, same major version as CapSkip's browser. Sent as useragent |
json |
int | No | 0 plain text, 1 JSON |
proxy |
string | No | The proxy your later requests go through |
proxytype |
string | No | HTTP, HTTPS, SOCKS5 or SOCKS5H |
Refused by the SDK before the request is made, as the server would refuse them:
- a missing
captcha_urlorpageurl; - a
captcha_urlthat is not https on captcha-delivery.com (or a subdomain), under/captcha/or/interstitial/, carryinginitialCid,hashandcid(server:ERROR_BAD_PARAMETERS); - a
captcha_urlwitht=bv: the block page bans the address that fetched it, so nothing sent from there can pass (server:ERROR_PROXY_BANNED); - a
pageurlthat is not http(s), or that points at captcha-delivery.com (server:ERROR_PAGEURL); - a
userAgentthat is not stock Chrome for Windows: Firefox, Safari, Edge, Opera, macOS, Linux, Android and iOS all fail (server:ERROR_WRONG_USERAGENT).
The Chrome major version is checked by CapSkip itself, against the browser
it solves with, and a mismatch comes back as ApiException('ERROR_WRONG_USERAGENT').
If the block page already contains the challenge iframe, take its src as it
is (an HTML-escaped & is fine). Most block pages build it with a script
from a var dd={...} object instead:
import json, re
from urllib.parse import urlencode
dd = json.loads(re.search(r"var dd=(\{[^}]*\})", html).group(1).replace("'", '"'))
path = "interstitial" if dd.get("rt") == "i" else "captcha"
q = {"initialCid": dd["cid"], "hash": dd["hsh"], "cid": dd["cookie"]}
if dd.get("t"):
q["t"] = dd["t"]
q["referer"] = page_url
q["s"] = dd["s"]
for k in ("e", "b"):
if dd.get(k):
q[k] = dd[k]
q["dm"] = "cd"
captcha_url = f"https://{dd.get('host', 'geo.captcha-delivery.com')}/{path}/?{urlencode(q)}"Each block page is a fresh challenge: submit it soon, and request the page again
for the next task rather than reusing an old captcha_url.
DataDome refuses the cookie from any other address, or with any other User-Agent:
- Solve from the address you will use the cookie from. Pass the proxy your requests go through; pass none if they come from the machine running CapSkip.
- Send the User-Agent you will use, or none. With none, CapSkip uses its browser's own, and the result names it.
- CapSkip's own DataDome proxy list. The app can hold a list of proxies for
DataDome (Settings, Captcha types, DataDome, Configure Proxies). A task sent
without a proxy is solved through that list, and the result then carries
proxy: that proxy ashost:port, without its login. The cookie only works through it. A proxy sent with the task always takes priority, and then noproxycomes back.
The SDK polls with json=1, where CapSkip returns the cookie in request with
userAgent and, when its list supplied the proxy, proxy next to it:
{
"status": 1,
"request": "datadome=FAPPpli4...; Max-Age=31536000; Domain=.example.com; Path=/; Secure; SameSite=Lax",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36",
"proxy": "203.0.113.10:8080"
}(In plain-text mode the same two values arrive in the X-DataDome-User-Agent
and X-DataDome-Proxy response headers.) See Return value.
# 2Captcha's argument order
result = solver.datadome(captcha_url, "https://www.example.com/", user_agent,
{"type": "HTTP", "uri": "login:password@203.0.113.10:8080"})
# No proxy, no User-Agent: CapSkip's browser, and its proxy list if it has one
result = solver.datadome(captcha_url, url="https://www.example.com/")
session.headers["User-Agent"] = result["userAgent"]
session.headers["Cookie"] = result["cookie"] # "datadome=..."
if "proxy" in result:
... # send this request through result["proxy"]DataDome may send a replacement datadome cookie on later responses; keep the
newest one, as a browser would. When the block page comes back anyway, build a
new captcha_url from it and submit a new task.
Uses recaptchaTimeout: the challenge is passed in a real browser.
hCaptcha is the checkbox widget behind many sign-up, login and checkout forms. CapSkip passes the widget for the sitekey and page you send and returns the token, ready to submit with the form. Invisible widgets and hCaptcha Enterprise use the same method. Public docs: https://capskip.com/api-docs/#hcaptcha.
The arguments follow 2Captcha's hcaptcha(sitekey, url, **kwargs), so code
written against it keeps working. pageurl is accepted for url.
| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | CapSkip API key |
method |
string | Yes | hcaptcha |
sitekey |
string | Yes | The widget's sitekey (data-sitekey) |
pageurl |
string | Yes | Full http or https URL of the page the widget is on |
invisible |
int | No | 1 for an invisible widget (data-size="invisible"). The SDK takes 1/0, True/False or "true"/"false", sends 1, and leaves it out when off |
data |
string | No | hCaptcha Enterprise: the rqdata value the page passes to hcaptcha.setData(). Single use; fetch a fresh one per task |
userAgent |
string | No | The User-Agent you will submit the form with. Used only when it matches CapSkip's browser. Sent as useragent |
json |
int | No | 0 plain text, 1 JSON |
proxy |
string | No | Proxy address |
proxytype |
string | No | HTTP, HTTPS, SOCKS5 or SOCKS5H |
Refused locally with ValidationException, as CapSkip would refuse them: a
missing sitekey (ERROR_RECAPTCHA_INVALID_SITEKEY), a pageurl that is missing
or not an absolute http(s) address (ERROR_PAGEURL), an invisible value other
than 0/1/true/false, and any parameter not in the table, 2Captcha's domain
among them.
The SDK polls with json=1, where CapSkip returns the token in request with
respKey and the User-Agent the token was earned with:
{
"status": 1,
"request": "P1_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.hadwYXNza2V5xQZ2...f3Kq8nV2",
"respKey": "E0_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjA0...9wB1",
"useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36"
}(In plain-text mode the User-Agent arrives in the X-HCaptcha-User-Agent
response header.) A task that hCaptcha refused the sitekey for fails with
ERROR_RECAPTCHA_INVALID_SITEKEY, one whose proxy could not be reached with
ERROR_PROXY_CONNECT_REFUSED; resubmitting the same task would fail again.
result = solver.hcaptcha("a5f74b19-9e45-40e0-b45d-47ff91b7a6c2",
"https://accounts.hcaptcha.com/demo")
# Invisible Enterprise widget, through a proxy
result = solver.hcaptcha(sitekey, page_url, invisible=1, data=rqdata,
proxy={"type": "HTTP", "uri": "login:password@203.0.113.10:8080"})
form["h-captcha-response"] = result["token"]
form["g-recaptcha-response"] = result["token"] # if the form has one
headers["User-Agent"] = result["userAgent"]Tokens are single use and expire about two minutes after they are issued.
Uses recaptchaTimeout: the widget is passed in a real browser.
Every solve method returns:
{
"captchaId": "12345",
"code": "TOKEN_OR_TEXT",
"userAgent": "..." # Turnstile only, when json=1 poll includes it
}ALTCHA additionally exposes token (the same string as code, named for the
form field it goes in) and number, the counter that solved it:
{
"captchaId": "12345",
"code": "eyJhbGdvcml0aG0iOiJTSEEtMjU2Iiwi…",
"token": "eyJhbGdvcml0aG0iOiJTSEEtMjU2Iiwi…",
"number": 9661,
}CaptchaFox and Friendly Captcha expose token, the same string as code, named
for the form field it goes in. CaptchaFox adds userAgent when the solve
reported one — the UA the browser minted the token under, not one you sent:
{
"captchaId": "12345",
"code": "177f50c25b845601e5c779cdb51b040d…",
"token": "177f50c25b845601e5c779cdb51b040d…",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) …",
}Capy returns an object rather than a token, expanded into the three fields the
target form takes (code keeps the raw answer):
{
"captchaId": "12345",
"code": {"captchakey": "PUZZLE_…", "challengekey": "…", "answer": "…", "respKey": ""},
"captchakey": "PUZZLE_Abc1dEFghIJKLM2no34P56q7rStu8v",
"challengekey": "BalY2gJaI8uA2SGVOZhqBQ3V0CYSNNGP",
"answer": "0xax8ex0xax84x0xkx7qx0x18x76x…",
"respKey": "",
}AWS WAF returns an object too, expanded into the pair the voucher endpoint
takes (code keeps the raw answer):
{
"captchaId": "12345",
"code": {"captcha_voucher": "…", "existing_token": "…"},
"captcha_voucher": "eyJ2b3VjaGVyIjoiRVhBTVBMRSJ9.9QwZcEXAMPLEvoucherJk4LmN0pQ",
"existing_token": "7d4c2a91-EXAMPLE-4f0b-9e6d-3a1b5c8e2f70:EQoAeXAMPLEtokenAAAA:Wm9wc2E=",
}DataDome returns a cookie. code keeps it exactly as DataDome set it, for a
cookie jar; cookie is the datadome=... pair alone, for a Cookie header.
userAgent is the User-Agent the cookie was earned with, and proxy appears
only when the task sent no proxy and CapSkip used its own DataDome proxy list:
{
"captchaId": "12345",
"code": "datadome=FAPPpli4...; Max-Age=31536000; Domain=.example.com; Path=/; Secure; SameSite=Lax",
"cookie": "datadome=FAPPpli4...",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36",
"proxy": "203.0.113.10:8080",
}hCaptcha exposes token, the same string as code, plus respKey and the
User-Agent the token was earned with. respKey is always present, and empty
when the widget handed out none:
{
"captchaId": "12345",
"code": "P1_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…",
"token": "P1_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…",
"respKey": "E0_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) …",
}GeeTest additionally expands its answer into challenge, validate, and
seccode (code keeps the raw JSON string):
{
"captchaId": "12345",
"code": '{"geetest_challenge":"...","geetest_validate":"...","geetest_seccode":"..."}',
"challenge": "...",
"validate": "...",
"seccode": "...",
}Convenience aliases mapped before sending to CapSkip:
| SDK alias | CapSkip API param |
|---|---|
url |
pageurl |
score |
min_score |
minScore |
min_score |
datas |
data-s |
data_s |
data-s |
apiServer |
api_server |
api_subdomain |
api_server |
challengeUrl / challengeURL |
challenge_url |
challengeJson / challengeJSON |
challenge_json |
userAgent / user_agent |
useragent |
captchaKey |
captchakey |
moduleScript |
module_script |
nomoduleScript / noModuleScript |
nomodule_script |
challengeScript |
challenge_script |
captchaScript |
captcha_script |
jsapi_script |
jsapiScript |
captchaUrl / captchaURL |
captcha_url |
proxy dict |
proxy + proxytype strings |
proxy = {"type": "HTTPS", "uri": "login:password@1.2.3.4:3128"}Unsupported parameters (e.g. numeric on image captcha, action on v2) raise ValidationException.
Submit without polling. Returns captcha ID string.
captcha_id = solver.send(
method="userrecaptcha",
googlekey="...",
pageurl="https://example.com",
)Poll once. Raises NetworkException while CAPCHA_NOT_READY.
from capskip import NetworkException
code = solver.get_result(captcha_id) # plain text
data = solver.get_result(captcha_id, json=1) # dict when json=1Same API as CapSkip, all methods are async:
from capskip import AsyncCapSkip
solver = AsyncCapSkip()
result = await solver.turnstile(sitekey="...", url="...")| Exception | When |
|---|---|
ValidationException |
Invalid/unsupported parameters |
NetworkException |
Connection error, or captcha not ready |
ApiException |
CapSkip API error response |
TimeoutException |
Polling timeout exceeded |
from capskip import ApiClient
client = ApiClient(host="127.0.0.1", port=8080)
client.in_(method="turnstile", key="capskip", sitekey="...", pageurl="...")
client.res(key="capskip", action="get", id="12345", json=1)