capmonster-client 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
capmonster/__init__.py ADDED
@@ -0,0 +1,79 @@
1
+ """CapMonster Cloud recognition API client."""
2
+
3
+ from .client import DEFAULT_SOFT_ID, CapMonster
4
+ from .errors import CAPMONSTER_ERRORS, CapMonsterError
5
+ from .tasks import (
6
+ alibaba,
7
+ altcha,
8
+ amazon,
9
+ basilisk,
10
+ binance,
11
+ castle,
12
+ complex_image,
13
+ datadome,
14
+ friendly_captcha,
15
+ funcaptcha,
16
+ geetest,
17
+ hcaptcha,
18
+ hunt,
19
+ image_to_text,
20
+ imperva,
21
+ mtcaptcha,
22
+ prosopo,
23
+ recaptcha_v2,
24
+ recaptcha_v2_enterprise,
25
+ recaptcha_v3,
26
+ recaptcha_v3_enterprise,
27
+ temu,
28
+ tendi,
29
+ tspd,
30
+ turnstile,
31
+ yidun,
32
+ )
33
+ from .types import (
34
+ TASK_CLASSES,
35
+ TASK_TYPES,
36
+ CapMonsterBalance,
37
+ RawApiResponse,
38
+ TaskResult,
39
+ TaskStatus,
40
+ )
41
+
42
+ __all__ = [
43
+ "CAPMONSTER_ERRORS",
44
+ "DEFAULT_SOFT_ID",
45
+ "TASK_CLASSES",
46
+ "TASK_TYPES",
47
+ "CapMonster",
48
+ "CapMonsterBalance",
49
+ "CapMonsterError",
50
+ "RawApiResponse",
51
+ "TaskResult",
52
+ "TaskStatus",
53
+ "alibaba",
54
+ "altcha",
55
+ "amazon",
56
+ "basilisk",
57
+ "binance",
58
+ "castle",
59
+ "complex_image",
60
+ "datadome",
61
+ "friendly_captcha",
62
+ "funcaptcha",
63
+ "geetest",
64
+ "hcaptcha",
65
+ "hunt",
66
+ "image_to_text",
67
+ "imperva",
68
+ "mtcaptcha",
69
+ "prosopo",
70
+ "recaptcha_v2",
71
+ "recaptcha_v2_enterprise",
72
+ "recaptcha_v3",
73
+ "recaptcha_v3_enterprise",
74
+ "temu",
75
+ "tendi",
76
+ "tspd",
77
+ "turnstile",
78
+ "yidun",
79
+ ]
capmonster/client.py ADDED
@@ -0,0 +1,299 @@
1
+ """Client for the CapMonster Cloud recognition API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from collections.abc import Mapping
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ from .errors import CapMonsterError, describe_error
12
+
13
+ _DEFAULT_BASE_URL = "https://api.capmonster.cloud"
14
+ _DEFAULT_TIMEOUT = 30.0
15
+
16
+ # Developer software id sent as `softId` on createTask.
17
+ #
18
+ # TODO: CapMonster Cloud's developer program is pending approval, so no
19
+ # softId has been issued yet. Leave this empty. When a numeric id is
20
+ # assigned, set it here. Requests omit `softId` until this constant or the
21
+ # client `soft_id` argument is a non-empty value.
22
+ #
23
+ # The createTask field name is `softId` (same name as the official client).
24
+ # https://docs.capmonster.cloud/docs/api/methods/create-task/
25
+ DEFAULT_SOFT_ID = ""
26
+
27
+
28
+ class CapMonster:
29
+ """Client for the CapMonster Cloud recognition API.
30
+
31
+ Args:
32
+ client_key: CapMonster Cloud client key.
33
+ base_url: Custom API base URL (defaults to `https://api.capmonster.cloud`).
34
+ timeout: Request timeout in seconds (defaults to 30.0).
35
+ soft_id: Developer software id. Empty until the developer program assigns one.
36
+
37
+ Examples:
38
+ >>> from capmonster import CapMonster
39
+ >>> with CapMonster("your_client_key") as client:
40
+ ... balance = client.get_balance()
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ client_key: str,
46
+ *,
47
+ base_url: str = _DEFAULT_BASE_URL,
48
+ timeout: float = _DEFAULT_TIMEOUT,
49
+ soft_id: str | int | None = None,
50
+ ) -> None:
51
+ if not client_key or not client_key.strip():
52
+ raise CapMonsterError("Client key is required", 1, "MISSING_CLIENT_KEY")
53
+
54
+ self._client_key = client_key.strip()
55
+ self._soft_id: str | int = DEFAULT_SOFT_ID if soft_id is None else soft_id
56
+ self._http = httpx.Client(
57
+ base_url=base_url.rstrip("/"),
58
+ timeout=timeout,
59
+ headers={
60
+ "Content-Type": "application/json",
61
+ "Accept": "application/json",
62
+ },
63
+ )
64
+
65
+ def close(self) -> None:
66
+ """Close the underlying HTTP client."""
67
+ self._http.close()
68
+
69
+ def __enter__(self) -> CapMonster:
70
+ return self
71
+
72
+ def __exit__(self, *exc: object) -> None:
73
+ self.close()
74
+
75
+ def get_balance(self) -> dict[str, float]:
76
+ """Retrieve the available account balance.
77
+
78
+ Returns:
79
+ Dictionary with a ``balance`` amount.
80
+
81
+ Examples:
82
+ >>> with CapMonster("your_key") as client:
83
+ ... balance = client.get_balance()
84
+ ... print(balance["balance"])
85
+ """
86
+ response = self._post("/getBalance", {"clientKey": self._client_key})
87
+ return {"balance": float(response.get("balance", 0.0))}
88
+
89
+ def create_task(
90
+ self,
91
+ task: Mapping[str, Any],
92
+ *,
93
+ soft_id: str | int | None = None,
94
+ callback_url: str | None = None,
95
+ ) -> int:
96
+ """Submit a new captcha recognition task.
97
+
98
+ ``softId`` is included only when ``DEFAULT_SOFT_ID`` or ``soft_id`` is set.
99
+
100
+ Args:
101
+ task: Task dictionary (must include ``type``).
102
+ soft_id: Override the client software id for this call.
103
+ callback_url: Optional URL that receives the getTaskResult payload.
104
+
105
+ Returns:
106
+ The submitted numeric task ID.
107
+
108
+ Examples:
109
+ >>> with CapMonster("your_key") as client:
110
+ ... task_id = client.create_task({
111
+ ... "type": "TurnstileTask",
112
+ ... "websiteURL": "https://example.com",
113
+ ... "websiteKey": "0x4AAAAAAA...",
114
+ ... })
115
+ """
116
+ payload: dict[str, Any] = {
117
+ "clientKey": self._client_key,
118
+ "task": dict(task),
119
+ }
120
+ chosen = self._soft_id if soft_id is None else soft_id
121
+ if has_soft_id(chosen):
122
+ payload["softId"] = format_soft_id(chosen)
123
+ if callback_url:
124
+ payload["callbackUrl"] = callback_url
125
+
126
+ response = self._post("/createTask", payload)
127
+ task_id = normalize_task_id(response.get("taskId"))
128
+ if task_id is None:
129
+ raise CapMonsterError(
130
+ "Response missing taskId",
131
+ response.get("errorId", 0),
132
+ response.get("errorCode"),
133
+ 200,
134
+ response,
135
+ )
136
+ return task_id
137
+
138
+ def get_task_result(self, task_id: int | str) -> dict[str, Any]:
139
+ """Query the status and solution of a submitted task.
140
+
141
+ ``CAPTCHA_NOT_READY`` is returned as ``{"status": "processing"}``.
142
+
143
+ Args:
144
+ task_id: Task identifier returned by ``create_task``.
145
+
146
+ Returns:
147
+ Dictionary containing ``status`` and optional ``solution``.
148
+ """
149
+ normalized = normalize_task_id(task_id)
150
+ if normalized is None:
151
+ raise CapMonsterError(f"Invalid taskId: {task_id}", 1, "WRONG_CAPTCHA_ID")
152
+
153
+ try:
154
+ response = self._post(
155
+ "/getTaskResult",
156
+ {"clientKey": self._client_key, "taskId": normalized},
157
+ )
158
+ except CapMonsterError as err:
159
+ if err.error_code == "CAPTCHA_NOT_READY":
160
+ return {"status": "processing", "solution": {}}
161
+ raise
162
+
163
+ return {
164
+ "status": response.get("status", "processing"),
165
+ "solution": response.get("solution", {}),
166
+ }
167
+
168
+ def wait_for_result(
169
+ self,
170
+ task_id: int | str,
171
+ *,
172
+ timeout: float = 120.0,
173
+ interval: float = 3.0,
174
+ ) -> dict[str, Any]:
175
+ """Poll ``get_task_result`` until the solution is ready.
176
+
177
+ Args:
178
+ task_id: Task identifier.
179
+ timeout: Maximum wait time in seconds (defaults to 120.0).
180
+ interval: Delay between poll attempts in seconds (defaults to 3.0).
181
+
182
+ Returns:
183
+ The solution dictionary.
184
+
185
+ Raises:
186
+ CapMonsterError: If polling times out or the task fails.
187
+ """
188
+ start_time = time.monotonic()
189
+
190
+ while True:
191
+ res = self.get_task_result(task_id)
192
+ if res.get("status") == "ready" and res.get("solution"):
193
+ solution = res["solution"]
194
+ if isinstance(solution, dict):
195
+ return solution
196
+ return {"value": solution}
197
+
198
+ if time.monotonic() - start_time >= timeout:
199
+ raise CapMonsterError(
200
+ "Timed out waiting for captcha solution",
201
+ 0,
202
+ "TIMEOUT",
203
+ )
204
+
205
+ time.sleep(interval)
206
+
207
+ def solve(
208
+ self,
209
+ task: Mapping[str, Any],
210
+ *,
211
+ timeout: float = 120.0,
212
+ interval: float = 3.0,
213
+ soft_id: str | int | None = None,
214
+ callback_url: str | None = None,
215
+ ) -> dict[str, Any]:
216
+ """Submit a task and wait for its solution.
217
+
218
+ Args:
219
+ task: Task dictionary.
220
+ timeout: Maximum wait time in seconds.
221
+ interval: Delay between poll attempts in seconds.
222
+ soft_id: Override the client software id for this call.
223
+ callback_url: Optional result callback URL.
224
+
225
+ Returns:
226
+ The solution dictionary.
227
+ """
228
+ task_id = self.create_task(task, soft_id=soft_id, callback_url=callback_url)
229
+ return self.wait_for_result(task_id, timeout=timeout, interval=interval)
230
+
231
+ def _post(self, path: str, json_data: Any) -> dict[str, Any]:
232
+ try:
233
+ res = self._http.post(path, json=json_data)
234
+ except httpx.HTTPError as err:
235
+ raise CapMonsterError(str(err), 0) from err
236
+
237
+ try:
238
+ data = res.json()
239
+ except ValueError:
240
+ data = None
241
+
242
+ if res.is_error:
243
+ error_id = data.get("errorId", 0) if isinstance(data, dict) else 0
244
+ error_code = data.get("errorCode") if isinstance(data, dict) else None
245
+ error_description = data.get("errorDescription") if isinstance(data, dict) else None
246
+ if isinstance(data, dict) and (error_description or error_code or error_id):
247
+ error_msg = describe_error(error_code, error_description, error_id)
248
+ else:
249
+ error_msg = res.text or f"HTTP {res.status_code}"
250
+ raise CapMonsterError(error_msg, error_id, error_code, res.status_code, data)
251
+
252
+ if not isinstance(data, dict):
253
+ raise CapMonsterError(
254
+ "Unexpected non-JSON response from CapMonster Cloud API",
255
+ 0,
256
+ None,
257
+ res.status_code,
258
+ res.text,
259
+ )
260
+
261
+ if data.get("errorId", 0) != 0:
262
+ error_id = int(data.get("errorId", 0))
263
+ error_code = data.get("errorCode")
264
+ msg = describe_error(error_code, data.get("errorDescription"), error_id)
265
+ raise CapMonsterError(msg, error_id, error_code, res.status_code, data)
266
+
267
+ return data
268
+
269
+
270
+ def has_soft_id(value: str | int | None) -> bool:
271
+ """True when a softId should be sent. Empty string and 0 are treated as unset."""
272
+ if value is None:
273
+ return False
274
+ if isinstance(value, str):
275
+ return value.strip() != ""
276
+ return value != 0
277
+
278
+
279
+ def format_soft_id(value: str | int) -> str | int:
280
+ """CapMonster expects a numeric softId. Numeric strings are sent as integers."""
281
+ if isinstance(value, int):
282
+ return value
283
+ trimmed = value.strip()
284
+ if trimmed.isdigit():
285
+ return int(trimmed)
286
+ return trimmed
287
+
288
+
289
+ def normalize_task_id(task_id: Any) -> int | None:
290
+ """Return a positive integer task id, or None when the value cannot be used."""
291
+ if task_id is None or task_id == "":
292
+ return None
293
+ try:
294
+ value = int(task_id)
295
+ except (TypeError, ValueError):
296
+ return None
297
+ if value <= 0:
298
+ return None
299
+ return value
capmonster/errors.py ADDED
@@ -0,0 +1,96 @@
1
+ """Error types for the CapMonster Cloud API client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ # Documented codes: https://docs.capmonster.cloud/docs/api/api-errors/
8
+ CAPMONSTER_ERRORS: dict[str, str] = {
9
+ "ERROR_KEY_DOES_NOT_EXIST": "API key not found or provided in an incorrect format",
10
+ "ERROR_PROXY_CREDENTIALS_INVALID_CHARACTER": (
11
+ "Proxy login or password contains an invalid character"
12
+ ),
13
+ "ERROR_ZERO_BALANCE": "Insufficient funds on the account to solve captchas",
14
+ "ERROR_TOO_BIG_CAPTCHA_FILESIZE": (
15
+ "The uploaded image exceeds the maximum allowed size of 50 KB"
16
+ ),
17
+ "ERROR_ZERO_CAPTCHA_FILESIZE": "The image size is too small (less than 100 bytes)",
18
+ "ERROR_NO_SUCH_CAPCHA_ID": "Captcha task id was not found",
19
+ "WRONG_CAPTCHA_ID": "Captcha task id is missing, expired, or invalid",
20
+ "ERROR_CAPTCHA_UNSOLVABLE": (
21
+ "The captcha could not be solved. Unsolved tasks are not charged"
22
+ ),
23
+ "CAPTCHA_NOT_READY": "The task is still being solved",
24
+ "ERROR_IP_NOT_ALLOWED": "Requests from this IP with the current key are denied",
25
+ "ERROR_IP_BANNED": "The IP address has been temporarily blocked due to invalid requests",
26
+ "ERROR_NO_SUCH_METHOD": "An incorrect task type was specified",
27
+ "ERROR_TOO_MUCH_REQUESTS": "The allowed request limit has been exceeded",
28
+ "ERROR_DOMAIN_NOT_ALLOWED": "Solving captchas from this domain is forbidden",
29
+ "ERROR_TOKEN_EXPIRED": "The provided token or challenge has expired",
30
+ "ERROR_RECAPTCHA_INVALID_SITEKEY": "An invalid websiteKey was provided",
31
+ "ERROR_RECAPTCHA_INVALID_DOMAIN": (
32
+ "The domain does not match the sitekey, or websiteURL is not a full http(s) URL"
33
+ ),
34
+ "ERROR_RECAPTCHA_TIMEOUT": "The captcha solving time has expired",
35
+ "ERROR_IP_BLOCKED": "Access from this IP is blocked due to a high number of failed requests",
36
+ "ERROR_PROXY_CONNECT_REFUSED": "The proxy server refused the connection",
37
+ "ERROR_PROXY_BANNED": "The specified proxy is blocked by the target website",
38
+ "ERROR_PROXY_MISSING": "Proxy parameters are missing or proxyType is incorrect",
39
+ "ERROR_PROXY_NOT_AUTHORISED": "Proxy login or password is incorrect",
40
+ "ERROR_PROXY_READ_TIMEOUT": "The response timed out while using the proxy",
41
+ "ERROR_TASK_NOT_SUPPORTED": "The specified task type is unsupported or invalid",
42
+ "ERROR_TASK_ABSENT": "The task object is missing or the JSON is invalid",
43
+ "ERROR_WRONG_USERAGENT": "An invalid User-Agent was provided",
44
+ "ERROR_SERVICE_NOT_AVAILABLE": "The service is temporarily unavailable",
45
+ "ERROR_INVALID_TASK": "The task is syntactically valid but contains invalid data",
46
+ "TIMEOUT": "Polling timed out before the captcha was solved",
47
+ "MISSING_CLIENT_KEY": "Client key is required",
48
+ }
49
+
50
+
51
+ class CapMonsterError(Exception):
52
+ """Exception raised for API errors or failed requests.
53
+
54
+ Args:
55
+ message: Human-readable error description.
56
+ error_id: Numeric error ID returned by CapMonster Cloud (0 is success).
57
+ error_code: String code (e.g. `ERROR_KEY_DOES_NOT_EXIST`).
58
+ status_code: HTTP status code or 0 for client/transport errors.
59
+ response: Raw response body, if available.
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ message: str,
65
+ error_id: int = 0,
66
+ error_code: str | None = None,
67
+ status_code: int = 0,
68
+ response: Any = None,
69
+ ) -> None:
70
+ super().__init__(message)
71
+ self.message = message
72
+ self.error_id = error_id
73
+ self.error_code = error_code
74
+ self.status_code = status_code
75
+ self.response = response
76
+
77
+ def __repr__(self) -> str:
78
+ return (
79
+ f"CapMonsterError(message={self.message!r}, "
80
+ f"error_id={self.error_id}, error_code={self.error_code!r})"
81
+ )
82
+
83
+
84
+ def describe_error(
85
+ error_code: str | None,
86
+ error_description: str | None,
87
+ error_id: int,
88
+ ) -> str:
89
+ """Prefer the API description, then the local catalog, then the raw code."""
90
+ if error_description:
91
+ return error_description
92
+ if error_code and error_code in CAPMONSTER_ERRORS:
93
+ return CAPMONSTER_ERRORS[error_code]
94
+ if error_code:
95
+ return error_code
96
+ return f"API error #{error_id}"
capmonster/tasks.py ADDED
@@ -0,0 +1,230 @@
1
+ """Task builders for documented CapMonster Cloud captcha types.
2
+
3
+ Each helper sets `type` (and `class`, when the docs require `CustomTask`)
4
+ and forwards the remaining API fields unchanged. Field names match the JSON
5
+ documented at https://docs.capmonster.cloud/docs/captchas/.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+
13
+ def _task(task_type: str, fields: dict[str, Any], task_class: str | None = None) -> dict[str, Any]:
14
+ task: dict[str, Any] = {"type": task_type}
15
+ if task_class is not None:
16
+ task["class"] = task_class
17
+ task.update(fields)
18
+ return task
19
+
20
+
21
+ def recaptcha_v2(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
22
+ """reCAPTCHA v2 (`RecaptchaV2Task`)."""
23
+ return _task(
24
+ "RecaptchaV2Task",
25
+ {"websiteURL": website_url, "websiteKey": website_key, **extra},
26
+ )
27
+
28
+
29
+ def recaptcha_v2_enterprise(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
30
+ """reCAPTCHA v2 Enterprise (`RecaptchaV2EnterpriseTask`)."""
31
+ return _task(
32
+ "RecaptchaV2EnterpriseTask",
33
+ {"websiteURL": website_url, "websiteKey": website_key, **extra},
34
+ )
35
+
36
+
37
+ def recaptcha_v3(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
38
+ """reCAPTCHA v3 (`RecaptchaV3TaskProxyless`). Pass isEnterprise=True for v3 Enterprise."""
39
+ return _task(
40
+ "RecaptchaV3TaskProxyless",
41
+ {"websiteURL": website_url, "websiteKey": website_key, **extra},
42
+ )
43
+
44
+
45
+ def recaptcha_v3_enterprise(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
46
+ """reCAPTCHA v3 Enterprise (`RecaptchaV3EnterpriseTask`)."""
47
+ return _task(
48
+ "RecaptchaV3EnterpriseTask",
49
+ {"websiteURL": website_url, "websiteKey": website_key, **extra},
50
+ )
51
+
52
+
53
+ def turnstile(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
54
+ """Cloudflare Turnstile, Challenge, and Waiting Room (`TurnstileTask`)."""
55
+ return _task("TurnstileTask", {"websiteURL": website_url, "websiteKey": website_key, **extra})
56
+
57
+
58
+ def geetest(website_url: str, gt: str, **extra: Any) -> dict[str, Any]:
59
+ """GeeTest v3 or v4 (`GeeTestTask`). Pass version and challenge via extra."""
60
+ return _task("GeeTestTask", {"websiteURL": website_url, "gt": gt, **extra})
61
+
62
+
63
+ def image_to_text(body: str, **extra: Any) -> dict[str, Any]:
64
+ """ImageToTextTask. `body` is a single-line Base64 image."""
65
+ return _task("ImageToTextTask", {"body": body, **extra})
66
+
67
+
68
+ def complex_image(class_name: str, **extra: Any) -> dict[str, Any]:
69
+ """ComplexImageTask. `class_name` is recaptcha, hcaptcha, funcaptcha, or recognition."""
70
+ return _task("ComplexImageTask", extra, class_name)
71
+
72
+
73
+ def datadome(website_url: str, metadata: dict[str, Any], **extra: Any) -> dict[str, Any]:
74
+ """DataDome (`CustomTask` / `DataDome`). Requires your own proxy in extra."""
75
+ return _task(
76
+ "CustomTask",
77
+ {"websiteURL": website_url, "metadata": metadata, **extra},
78
+ "DataDome",
79
+ )
80
+
81
+
82
+ def tendi(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
83
+ """TenDI / Tencent captcha (`CustomTask` / `TenDI`).
84
+
85
+ CapMonster documents this captcha as TenDI, not Tenable.
86
+ """
87
+ return _task(
88
+ "CustomTask",
89
+ {"websiteURL": website_url, "websiteKey": website_key, **extra},
90
+ "TenDI",
91
+ )
92
+
93
+
94
+ def amazon(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
95
+ """Amazon / AWS WAF (`AmazonTask`)."""
96
+ return _task("AmazonTask", {"websiteURL": website_url, "websiteKey": website_key, **extra})
97
+
98
+
99
+ def binance(website_url: str, website_key: str, validate_id: str, **extra: Any) -> dict[str, Any]:
100
+ """Binance captcha (`BinanceTask`)."""
101
+ return _task(
102
+ "BinanceTask",
103
+ {
104
+ "websiteURL": website_url,
105
+ "websiteKey": website_key,
106
+ "validateId": validate_id,
107
+ **extra,
108
+ },
109
+ )
110
+
111
+
112
+ def funcaptcha(website_url: str, website_public_key: str, **extra: Any) -> dict[str, Any]:
113
+ """Arkose Labs FunCaptcha (`FunCaptchaTask`)."""
114
+ return _task(
115
+ "FunCaptchaTask",
116
+ {"websiteURL": website_url, "websitePublicKey": website_public_key, **extra},
117
+ )
118
+
119
+
120
+ def hcaptcha(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
121
+ """hCaptcha (`HCaptchaTask`)."""
122
+ return _task("HCaptchaTask", {"websiteURL": website_url, "websiteKey": website_key, **extra})
123
+
124
+
125
+ def basilisk(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
126
+ """Basilisk (`CustomTask` / `Basilisk`)."""
127
+ return _task(
128
+ "CustomTask",
129
+ {"websiteURL": website_url, "websiteKey": website_key, **extra},
130
+ "Basilisk",
131
+ )
132
+
133
+
134
+ def imperva(website_url: str, metadata: dict[str, Any], **extra: Any) -> dict[str, Any]:
135
+ """Imperva / Incapsula (`CustomTask` / `Imperva`)."""
136
+ return _task(
137
+ "CustomTask",
138
+ {"websiteURL": website_url, "metadata": metadata, **extra},
139
+ "Imperva",
140
+ )
141
+
142
+
143
+ def prosopo(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
144
+ """Prosopo (`ProsopoTask`)."""
145
+ return _task("ProsopoTask", {"websiteURL": website_url, "websiteKey": website_key, **extra})
146
+
147
+
148
+ def yidun(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
149
+ """Yidun (`YidunTask`)."""
150
+ return _task("YidunTask", {"websiteURL": website_url, "websiteKey": website_key, **extra})
151
+
152
+
153
+ def mtcaptcha(website_url: str, website_key: str, **extra: Any) -> dict[str, Any]:
154
+ """MTCaptcha (`MTCaptchaTask`)."""
155
+ return _task("MTCaptchaTask", {"websiteURL": website_url, "websiteKey": website_key, **extra})
156
+
157
+
158
+ def altcha(
159
+ website_url: str,
160
+ website_key: str,
161
+ metadata: dict[str, Any],
162
+ **extra: Any,
163
+ ) -> dict[str, Any]:
164
+ """Altcha (`CustomTask` / `altcha`)."""
165
+ return _task(
166
+ "CustomTask",
167
+ {"websiteURL": website_url, "websiteKey": website_key, "metadata": metadata, **extra},
168
+ "altcha",
169
+ )
170
+
171
+
172
+ def friendly_captcha(
173
+ website_url: str,
174
+ website_key: str,
175
+ metadata: dict[str, Any],
176
+ **extra: Any,
177
+ ) -> dict[str, Any]:
178
+ """Friendly Captcha (`CustomTask` / `friendly`)."""
179
+ return _task(
180
+ "CustomTask",
181
+ {"websiteURL": website_url, "websiteKey": website_key, "metadata": metadata, **extra},
182
+ "friendly",
183
+ )
184
+
185
+
186
+ def temu(website_url: str, metadata: dict[str, Any], **extra: Any) -> dict[str, Any]:
187
+ """Temu (`CustomTask` / `Temu`)."""
188
+ return _task("CustomTask", {"websiteURL": website_url, "metadata": metadata, **extra}, "Temu")
189
+
190
+
191
+ def castle(
192
+ website_url: str,
193
+ website_key: str,
194
+ metadata: dict[str, Any],
195
+ **extra: Any,
196
+ ) -> dict[str, Any]:
197
+ """Castle (`CustomTask` / `Castle`)."""
198
+ return _task(
199
+ "CustomTask",
200
+ {"websiteURL": website_url, "websiteKey": website_key, "metadata": metadata, **extra},
201
+ "Castle",
202
+ )
203
+
204
+
205
+ def tspd(
206
+ website_url: str,
207
+ user_agent: str,
208
+ metadata: dict[str, Any],
209
+ **extra: Any,
210
+ ) -> dict[str, Any]:
211
+ """TSPD (`CustomTask` / `tspd`)."""
212
+ return _task(
213
+ "CustomTask",
214
+ {"websiteURL": website_url, "userAgent": user_agent, "metadata": metadata, **extra},
215
+ "tspd",
216
+ )
217
+
218
+
219
+ def hunt(website_url: str, metadata: dict[str, Any], **extra: Any) -> dict[str, Any]:
220
+ """Hunt CAPTCHA (`CustomTask` / `HUNT`)."""
221
+ return _task("CustomTask", {"websiteURL": website_url, "metadata": metadata, **extra}, "HUNT")
222
+
223
+
224
+ def alibaba(website_url: str, metadata: dict[str, Any], **extra: Any) -> dict[str, Any]:
225
+ """Alibaba captcha (`CustomTask` / `alibaba`)."""
226
+ return _task(
227
+ "CustomTask",
228
+ {"websiteURL": website_url, "metadata": metadata, **extra},
229
+ "alibaba",
230
+ )
capmonster/types.py ADDED
@@ -0,0 +1,76 @@
1
+ """Type definitions for the CapMonster Cloud API client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal, TypedDict
6
+
7
+ ProxyType = Literal["http", "https", "socks4", "socks5"]
8
+ TaskStatus = Literal["processing", "ready"]
9
+
10
+ # Documented `type` values. Custom captchas also set `class` (see TASK_CLASSES).
11
+ TASK_TYPES: dict[str, str] = {
12
+ "recaptcha_v2": "RecaptchaV2Task",
13
+ "recaptcha_v2_proxyless": "RecaptchaV2TaskProxyless",
14
+ "no_captcha": "NoCaptchaTask",
15
+ "no_captcha_proxyless": "NoCaptchaTaskProxyless",
16
+ "recaptcha_v2_enterprise": "RecaptchaV2EnterpriseTask",
17
+ "recaptcha_v3": "RecaptchaV3TaskProxyless",
18
+ "recaptcha_v3_enterprise": "RecaptchaV3EnterpriseTask",
19
+ "turnstile": "TurnstileTask",
20
+ "geetest": "GeeTestTask",
21
+ "image_to_text": "ImageToTextTask",
22
+ "complex_image": "ComplexImageTask",
23
+ "custom": "CustomTask",
24
+ "funcaptcha": "FunCaptchaTask",
25
+ "hcaptcha": "HCaptchaTask",
26
+ "hcaptcha_proxyless": "HCaptchaTaskProxyless",
27
+ "amazon": "AmazonTask",
28
+ "binance": "BinanceTask",
29
+ "prosopo": "ProsopoTask",
30
+ "yidun": "YidunTask",
31
+ "mtcaptcha": "MTCaptchaTask",
32
+ }
33
+
34
+ # `class` values for CustomTask and ComplexImageTask.
35
+ TASK_CLASSES: dict[str, str] = {
36
+ "datadome": "DataDome",
37
+ "tendi": "TenDI",
38
+ "basilisk": "Basilisk",
39
+ "imperva": "Imperva",
40
+ "altcha": "altcha",
41
+ "friendly": "friendly",
42
+ "temu": "Temu",
43
+ "castle": "Castle",
44
+ "tspd": "tspd",
45
+ "hunt": "HUNT",
46
+ "alibaba": "alibaba",
47
+ "complex_recaptcha": "recaptcha",
48
+ "complex_hcaptcha": "hcaptcha",
49
+ "complex_funcaptcha": "funcaptcha",
50
+ "complex_recognition": "recognition",
51
+ }
52
+
53
+
54
+ class CapMonsterBalance(TypedDict):
55
+ """Account balance details."""
56
+
57
+ balance: float
58
+
59
+
60
+ class TaskResult(TypedDict, total=False):
61
+ """Result returned by getTaskResult."""
62
+
63
+ status: TaskStatus
64
+ solution: dict[str, Any]
65
+
66
+
67
+ class RawApiResponse(TypedDict, total=False):
68
+ """Envelope returned by CapMonster Cloud endpoints."""
69
+
70
+ errorId: int
71
+ errorCode: str | None
72
+ errorDescription: str | None
73
+ taskId: int | str
74
+ status: TaskStatus
75
+ solution: Any
76
+ balance: float
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.5
2
+ Name: capmonster-client
3
+ Version: 0.1.0
4
+ Summary: Python client for the CapMonster Cloud captcha recognition API
5
+ Project-URL: Repository, https://github.com/TysikGG/api-kit
6
+ Project-URL: Documentation, https://docs.capmonster.cloud
7
+ License-Expression: ISC
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: httpx>=0.27
11
+ Description-Content-Type: text/markdown
12
+
13
+ # capmonster-client
14
+
15
+ [![PyPI version](https://img.shields.io/pypi/v/capmonster-client.svg)](https://pypi.org/project/capmonster-client/)
16
+ [![Python versions](https://img.shields.io/pypi/pyversions/capmonster-client.svg)](https://pypi.org/project/capmonster-client/)
17
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
18
+
19
+ Typed Python client for the [CapMonster Cloud API](https://docs.capmonster.cloud). Supports reCAPTCHA v2/v3/Enterprise, Cloudflare Turnstile (including Challenge and Waiting Room), GeeTest, ImageToText, ComplexImage, DataDome, Amazon AWS WAF, TenDI (Tencent), Binance, and the other task types documented by CapMonster Cloud. Built on [HTTPX](https://www.python-httpx.org).
20
+
21
+ Official API documentation: <https://docs.capmonster.cloud>
22
+
23
+ The import package is `capmonster`. The distribution name is `capmonster-client` so it does not collide with other projects on PyPI.
24
+
25
+ ---
26
+
27
+ ## Features
28
+
29
+ - 🧩 **Documented task types** — Helpers for reCAPTCHA v2/v3/Enterprise, Turnstile, GeeTest, ImageToText, ComplexImage, DataDome, Amazon WAF, TenDI, Binance, FunCaptcha, hCaptcha, Basilisk, Imperva, Prosopo, Yidun, MTCaptcha, Altcha, Friendly Captcha, Temu, Castle, TSPD, Hunt, and Alibaba.
30
+ - ⏳ **Automated polling** — `wait_for_result()` and `solve()` poll until the solution is ready. `CAPTCHA_NOT_READY` stays in the processing state.
31
+ - 💰 **Balance tracking** — `get_balance()` reads the account balance and does not spend credit.
32
+ - 🏷️ **Developer id** — `softId` is omitted until you set it. CapMonster's developer program is still pending, so `DEFAULT_SOFT_ID` is empty.
33
+ - 🐍 **Modern Python** — Type annotations, context manager support (`with CapMonster(...) as client:`).
34
+ - 🚨 **Structured errors** — `CapMonsterError` carries `error_id` and `error_code` for every documented API error.
35
+
36
+ ---
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install capmonster-client
42
+ ```
43
+
44
+ Requires Python 3.10 or higher.
45
+
46
+ ---
47
+
48
+ ## Quick Start (Safe Check)
49
+
50
+ The quick check only inspects your account balance. It does not submit tasks or spend credits.
51
+
52
+ ```python
53
+ import os
54
+ from capmonster import CapMonster
55
+
56
+ with CapMonster(os.environ["CAPMONSTER_API_KEY"]) as client:
57
+ balance = client.get_balance()
58
+ print(f"Balance: {balance['balance']}")
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Usage Guide
64
+
65
+ ### 1. High-Level Solve (Recommended)
66
+
67
+ `client.solve()` submits the task and waits until the solution is ready:
68
+
69
+ ```python
70
+ import os
71
+ from capmonster import CapMonster, turnstile
72
+
73
+ with CapMonster(os.environ["CAPMONSTER_API_KEY"]) as client:
74
+ solution = client.solve(turnstile("https://example.com/", "0x4AAAAAAA..."))
75
+ print("Turnstile token:", solution["token"])
76
+ ```
77
+
78
+ ### 2. Solving reCAPTCHA v2
79
+
80
+ ```python
81
+ from capmonster import CapMonster, recaptcha_v2
82
+
83
+ with CapMonster(os.environ["CAPMONSTER_API_KEY"]) as client:
84
+ solution = client.solve(recaptcha_v2(
85
+ "https://lessons.zennolab.com/captchas/recaptcha/v2_simple.php?level=high",
86
+ "6Lcg7CMUAAAAANphynKgn9YAgA4tQ2KI_iqRyTwd",
87
+ ))
88
+ print(solution["gRecaptchaResponse"])
89
+ ```
90
+
91
+ ### 3. Manual Task Lifecycle
92
+
93
+ ```python
94
+ from capmonster import CapMonster, turnstile
95
+
96
+ with CapMonster(os.environ["CAPMONSTER_API_KEY"]) as client:
97
+ task_id = client.create_task(turnstile("https://example.com/", "0x4AAAAAAA..."))
98
+ result = client.get_task_result(task_id)
99
+ if result["status"] == "ready":
100
+ print("Solution:", result["solution"])
101
+ else:
102
+ print("Task is still processing...")
103
+ ```
104
+
105
+ You can also pass a raw `dict`. `type` is required. Custom captchas (DataDome, TenDI, and the others listed below) also require `class`.
106
+
107
+ ### 4. Developer software id
108
+
109
+ `create_task` sends `softId` only when it is set:
110
+
111
+ ```python
112
+ from capmonster import DEFAULT_SOFT_ID, CapMonster
113
+
114
+ print(DEFAULT_SOFT_ID) # ""
115
+
116
+ with CapMonster(os.environ["CAPMONSTER_API_KEY"], soft_id=12345) as client:
117
+ ...
118
+ ```
119
+
120
+ TODO: replace `DEFAULT_SOFT_ID` in `src/capmonster/client.py` once CapMonster assigns a numeric id.
121
+
122
+ ---
123
+
124
+ ## Task Types
125
+
126
+ | Captcha | `type` | `class` | Helper |
127
+ | :--- | :--- | :--- | :--- |
128
+ | reCAPTCHA v2 | `RecaptchaV2Task` | | `recaptcha_v2` |
129
+ | reCAPTCHA v2 Enterprise | `RecaptchaV2EnterpriseTask` | | `recaptcha_v2_enterprise` |
130
+ | reCAPTCHA v3 | `RecaptchaV3TaskProxyless` | | `recaptcha_v3` |
131
+ | reCAPTCHA v3 Enterprise | `RecaptchaV3EnterpriseTask` | | `recaptcha_v3_enterprise` |
132
+ | Cloudflare Turnstile / Challenge / Waiting Room | `TurnstileTask` | | `turnstile` |
133
+ | GeeTest v3 / v4 | `GeeTestTask` | | `geetest` |
134
+ | ImageToText | `ImageToTextTask` | | `image_to_text` |
135
+ | ComplexImage (click, grid, recognition) | `ComplexImageTask` | `recaptcha`, `hcaptcha`, `funcaptcha`, `recognition` | `complex_image` |
136
+ | DataDome | `CustomTask` | `DataDome` | `datadome` |
137
+ | TenDI (Tencent) | `CustomTask` | `TenDI` | `tendi` |
138
+ | Amazon AWS WAF | `AmazonTask` | | `amazon` |
139
+ | Binance | `BinanceTask` | | `binance` |
140
+ | FunCaptcha | `FunCaptchaTask` | | `funcaptcha` |
141
+ | hCaptcha | `HCaptchaTask` | | `hcaptcha` |
142
+ | Basilisk | `CustomTask` | `Basilisk` | `basilisk` |
143
+ | Imperva (Incapsula) | `CustomTask` | `Imperva` | `imperva` |
144
+ | Prosopo | `ProsopoTask` | | `prosopo` |
145
+ | Yidun | `YidunTask` | | `yidun` |
146
+ | MTCaptcha | `MTCaptchaTask` | | `mtcaptcha` |
147
+ | Altcha | `CustomTask` | `altcha` | `altcha` |
148
+ | Friendly Captcha | `CustomTask` | `friendly` | `friendly_captcha` |
149
+ | Temu | `CustomTask` | `Temu` | `temu` |
150
+ | Castle | `CustomTask` | `Castle` | `castle` |
151
+ | TSPD | `CustomTask` | `tspd` | `tspd` |
152
+ | Hunt | `CustomTask` | `HUNT` | `hunt` |
153
+ | Alibaba | `CustomTask` | `alibaba` | `alibaba` |
154
+
155
+ Legacy aliases `NoCaptchaTask`, `NoCaptchaTaskProxyless`, `RecaptchaV2TaskProxyless`, and `HCaptchaTaskProxyless` are accepted on the `type` field. Current docs use `RecaptchaV2Task` with proxy fields omitted when CapMonster's own proxies are enough.
156
+
157
+ TenDI is the Tencent captcha documented by CapMonster. It is not a Tenable product.
158
+
159
+ Parameter reference: <https://docs.capmonster.cloud/docs/captchas/>
160
+
161
+ ---
162
+
163
+ ## Error Handling
164
+
165
+ When the API returns an error, the client raises `CapMonsterError`:
166
+
167
+ ```python
168
+ from capmonster import CapMonster, CapMonsterError
169
+
170
+ try:
171
+ with CapMonster("invalid_key") as client:
172
+ client.get_balance()
173
+ except CapMonsterError as err:
174
+ print(f"CapMonster error #{err.error_id} [{err.error_code}]: {err.message}")
175
+ ```
176
+
177
+ Common error codes (full list: <https://docs.capmonster.cloud/docs/api/api-errors/>):
178
+
179
+ - `ERROR_KEY_DOES_NOT_EXIST` — Account key does not exist or is malformed.
180
+ - `ERROR_ZERO_BALANCE` — Account has no funds. Returned by `create_task`, not by `get_balance`.
181
+ - `ERROR_CAPTCHA_UNSOLVABLE` — The captcha could not be solved. Unsolved tasks are not charged.
182
+ - `CAPTCHA_NOT_READY` — Still solving. `get_task_result` maps this to `status: "processing"`.
183
+ - `ERROR_TASK_NOT_SUPPORTED` / `ERROR_NO_SUCH_METHOD` — Unknown `type`.
184
+ - `ERROR_INVALID_TASK` / `ERROR_TASK_ABSENT` — Task payload is missing or invalid.
185
+ - `ERROR_TOO_MUCH_REQUESTS` — Poll `get_task_result` no more than once every 2 seconds. The default interval is 3 seconds.
186
+ - `TIMEOUT` — `wait_for_result()` exceeded the configured timeout.
187
+
188
+ `CAPMONSTER_ERRORS` maps every documented code to a short description. The API `errorDescription` is used when CapMonster sends one.
189
+
190
+ ---
191
+
192
+ ## Live Tests
193
+
194
+ Unit tests mock HTTP and always run. The live suite calls the real API and runs only when `CAPMONSTER_API_KEY` is set. It checks balance, an invalid key, and `ERROR_ZERO_BALANCE` on `create_task`.
195
+
196
+ `create_task` is sent only when the account balance is exactly `0`. A funded key skips that assertion so the suite does not spend credit.
197
+
198
+ ```bash
199
+ cd capmonster/python
200
+ uv sync
201
+ uv run pytest -m "not live"
202
+ CAPMONSTER_API_KEY=your_key uv run pytest -m live
203
+ ```
204
+
205
+ Do not commit API keys.
206
+
207
+ ---
208
+
209
+ ## License
210
+
211
+ [ISC](LICENSE) © api-kit contributors
@@ -0,0 +1,9 @@
1
+ capmonster/__init__.py,sha256=srbljr3wN9rR6cTQzvH6uGbuOj6GntzMiHVdtgmxyZI,1373
2
+ capmonster/client.py,sha256=2Jnynz_BaANaHPZSPN8MzzJ0WWR4D1EptgsYfPcvAoY,9968
3
+ capmonster/errors.py,sha256=1I7sycm5oDCjKl8DJDe49A-ITEEj_tJBlNdYufl3vRk,4274
4
+ capmonster/tasks.py,sha256=uM_b241tD1xEoZ8j_futb_bERhmjnLIryYedrSNGvg0,7665
5
+ capmonster/types.py,sha256=2SipfWVTmUfwdfO8oksEoyDR9qL0lov2gihEmviFc1Y,2163
6
+ capmonster_client-0.1.0.dist-info/METADATA,sha256=uypg5KE1UyAgiBZLnZQIGO_jjwY2Ahof0P6fWbVfPlc,8256
7
+ capmonster_client-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
8
+ capmonster_client-0.1.0.dist-info/licenses/LICENSE,sha256=8S0uYmn_aeljwFudfBoV91HcIo79yab9s0yhMVsOHJ0,751
9
+ capmonster_client-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 api-kit contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.