Skip to content

Latest commit

 

History

History
1095 lines (844 loc) · 38.6 KB

API_REFERENCE.md

File metadata and controls

1095 lines (844 loc) · 38.6 KB

CapSkip API Reference

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.


Supported captcha types

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.


CapSkip (synchronous)

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
)

1. Image captcha — normal(file, json=0)

POST /in.php

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

GET /res.php

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

SDK usage

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.


2. reCAPTCHA v2 — recaptcha(sitekey, url, version='v2', ...)

POST /in.php

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.

GET /res.php

Same as image captcha poll parameters.

SDK usage

# 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"},
)

3. reCAPTCHA v3 — recaptcha(..., version='v3', ...)

POST /in.php

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.

SDK usage

result = solver.recaptcha(
    sitekey="...",
    url="https://example.com",
    version="v3",
    action="submit",
    min_score=0.7,          # or SDK alias: score=0.7
    enterprise=0,
)

4. Cloudflare Turnstile — turnstile(sitekey, url, ...)

POST /in.php

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

GET /res.php

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.

SDK usage

# 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

5. GeeTest v3 — geetest(gt, challenge, url, ...)

POST /in.php

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

Getting gt and challenge

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.

challenge is 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.

SDK usage

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 string

Post 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.


6. ALTCHA — altcha(url, ...)

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.

POST /in.php

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.

Getting the challenge

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_url and the challenge expired while the job queued, it fetches a fresh one automatically.

SDK usage

# 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 token

challenge_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.

Algorithms

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.


7. Capy Puzzle — capy(sitekey, url, ...)

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.

POST /in.php

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.me no longer resolves, although several solver services still document it. The live host is jp.api.capy.me; if a site's widget points somewhere else, pass that as api_server.

Getting the captcha key

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">.

GET /res.php

The answer is not a token. It is an object, which the SDK expands into three result fields. See Return value.

SDK usage

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"]

Timing

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.


8. CaptchaFox — captchafox(sitekey, url, ...)

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.

POST /in.php

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

Choosing the widget source

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.

The page URL has to match the key

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 types

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.

SDK usage

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.


9. Friendly Captcha — friendly_captcha(sitekey, url, ...)

Proof-of-work, with nothing shown on screen. The widget searches for values that hash below a difficulty the service sets per request.

POST /in.php

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

Version 1 and version 2

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.

Submitting the token

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.

SDK usage

# 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.


10. AWS WAF — amazon_waf(sitekey, iv=None, context=None, url=None, ...)

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.

POST /in.php

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 or widget page

  • Challenge page. The firewall answers with HTTP 405 and a small page carrying window.gokuProps and loading challenge.js and captcha.js from an awswaf.com host. Send gokuProps.key as sitekey, plus iv and context, and optionally the two script URLs. Any of those you leave out is read from pageurl at 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 its apiKey as sitekey and the jsapi.js URL as jsapiScript, with no iv or context.

GET /res.php

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.

SDK usage

# 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"]

Turning the voucher into the cookie

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.


11. DataDome — datadome(captcha_url, pageurl=None, userAgent=None, proxy=None, ...)

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.

POST /in.php

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_url or pageurl;
  • a captcha_url that is not https on captcha-delivery.com (or a subdomain), under /captcha/ or /interstitial/, carrying initialCid, hash and cid (server: ERROR_BAD_PARAMETERS);
  • a captcha_url with t=bv: the block page bans the address that fetched it, so nothing sent from there can pass (server: ERROR_PROXY_BANNED);
  • a pageurl that is not http(s), or that points at captcha-delivery.com (server: ERROR_PAGEURL);
  • a userAgent that 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').

Building the captcha_url

If the block page already contains the challenge iframe, take its src as it is (an HTML-escaped &amp; 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.

The cookie belongs to one IP and one User-Agent

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 as host:port, without its login. The cookie only works through it. A proxy sent with the task always takes priority, and then no proxy comes back.

GET /res.php

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.

SDK usage

# 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.


12. hCaptcha — hcaptcha(sitekey, url, ...)

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.

POST /in.php

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.

GET /res.php

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.

SDK usage

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.


Return value

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": "...",
}

SDK parameter aliases

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.


Manual workflow

send(**kwargs)

Submit without polling. Returns captcha ID string.

captcha_id = solver.send(
    method="userrecaptcha",
    googlekey="...",
    pageurl="https://example.com",
)

get_result(id_, json=0)

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=1

AsyncCapSkip

Same API as CapSkip, all methods are async:

from capskip import AsyncCapSkip

solver = AsyncCapSkip()
result = await solver.turnstile(sitekey="...", url="...")

Exceptions

Exception When
ValidationException Invalid/unsupported parameters
NetworkException Connection error, or captcha not ready
ApiException CapSkip API error response
TimeoutException Polling timeout exceeded

Low-level HTTP (ApiClient)

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)