solvecaptcha 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.
- solvecaptcha/__init__.py +13 -0
- solvecaptcha/client.py +510 -0
- solvecaptcha/errors.py +33 -0
- solvecaptcha/types.py +20 -0
- solvecaptcha-0.1.0.dist-info/METADATA +227 -0
- solvecaptcha-0.1.0.dist-info/RECORD +8 -0
- solvecaptcha-0.1.0.dist-info/WHEEL +4 -0
- solvecaptcha-0.1.0.dist-info/licenses/LICENSE +15 -0
solvecaptcha/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""SolveCaptcha Python Client."""
|
|
2
|
+
|
|
3
|
+
from .client import DEFAULT_SOFT_ID, SolveCaptchaClient
|
|
4
|
+
from .errors import SolveCaptchaError
|
|
5
|
+
from .types import CaptchaTask, GeeTestResult
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"DEFAULT_SOFT_ID",
|
|
9
|
+
"CaptchaTask",
|
|
10
|
+
"GeeTestResult",
|
|
11
|
+
"SolveCaptchaClient",
|
|
12
|
+
"SolveCaptchaError",
|
|
13
|
+
]
|
solvecaptcha/client.py
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
"""SolveCaptcha API client implementation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .errors import ERROR_DESCRIPTIONS, SolveCaptchaError
|
|
10
|
+
from .types import GeeTestResult
|
|
11
|
+
|
|
12
|
+
DEFAULT_SOFT_ID = ""
|
|
13
|
+
DEFAULT_BASE_URL = "https://api.solvecaptcha.com"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SolveCaptchaClient:
|
|
17
|
+
"""Client for the SolveCaptcha human and AI captcha solving service.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
api_key: SolveCaptcha API key.
|
|
21
|
+
base_url: Base URL of the API. Defaults to https://api.solvecaptcha.com.
|
|
22
|
+
timeout: HTTP request timeout in seconds. Defaults to 30.0.
|
|
23
|
+
soft_id: Optional software/partner identifier.
|
|
24
|
+
default_polling_timeout: Default timeout in seconds when waiting for solutions.
|
|
25
|
+
default_polling_interval: Default interval in seconds between status polls.
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
>>> from solvecaptcha import SolveCaptchaClient
|
|
29
|
+
>>> client = SolveCaptchaClient(api_key="your_api_key")
|
|
30
|
+
>>> balance = client.get_balance()
|
|
31
|
+
>>> print(balance)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
api_key: str,
|
|
37
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
38
|
+
timeout: float = 30.0,
|
|
39
|
+
soft_id: str | int | None = None,
|
|
40
|
+
default_polling_timeout: float = 120.0,
|
|
41
|
+
default_polling_interval: float = 5.0,
|
|
42
|
+
) -> None:
|
|
43
|
+
if not api_key or not api_key.strip():
|
|
44
|
+
raise SolveCaptchaError(
|
|
45
|
+
"api_key is required to initialize SolveCaptchaClient", "ERROR_KEY_DOES_NOT_EXIST"
|
|
46
|
+
)
|
|
47
|
+
self.api_key = api_key.strip()
|
|
48
|
+
self.base_url = base_url.rstrip("/")
|
|
49
|
+
self.soft_id = str(soft_id) if soft_id is not None else DEFAULT_SOFT_ID
|
|
50
|
+
self.polling_timeout = default_polling_timeout
|
|
51
|
+
self.polling_interval = default_polling_interval
|
|
52
|
+
self._client = httpx.Client(base_url=self.base_url, timeout=timeout)
|
|
53
|
+
|
|
54
|
+
def __enter__(self) -> "SolveCaptchaClient":
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
58
|
+
self.close()
|
|
59
|
+
|
|
60
|
+
def close(self) -> None:
|
|
61
|
+
"""Closes the underlying HTTP client session."""
|
|
62
|
+
self._client.close()
|
|
63
|
+
|
|
64
|
+
def get_balance(self) -> float:
|
|
65
|
+
"""Retrieves the current account balance in USD.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The account balance as a float.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
SolveCaptchaError: If the server returns an error or unexpected response.
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
>>> client = SolveCaptchaClient(api_key="key")
|
|
75
|
+
>>> balance = client.get_balance()
|
|
76
|
+
>>> print(f"Balance: ${balance:.4f}")
|
|
77
|
+
"""
|
|
78
|
+
raw = self._query_result({"action": "getbalance"})
|
|
79
|
+
try:
|
|
80
|
+
return float(raw)
|
|
81
|
+
except ValueError as exc:
|
|
82
|
+
raise SolveCaptchaError(f"Failed to parse balance: {raw}", "PARSE_ERROR", raw) from exc
|
|
83
|
+
|
|
84
|
+
def solve_image(
|
|
85
|
+
self,
|
|
86
|
+
body: str,
|
|
87
|
+
phrase: int | None = None,
|
|
88
|
+
regsense: int | None = None,
|
|
89
|
+
numeric: int | None = None,
|
|
90
|
+
calc: int | None = None,
|
|
91
|
+
min_len: int | None = None,
|
|
92
|
+
max_len: int | None = None,
|
|
93
|
+
language: int | None = None,
|
|
94
|
+
textinstructions: str | None = None,
|
|
95
|
+
) -> str:
|
|
96
|
+
"""Solves a normal image-based captcha from base64 data.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
body: Base64-encoded image string.
|
|
100
|
+
phrase: 0 = single word, 1 = multiple words.
|
|
101
|
+
regsense: 0 = case-insensitive, 1 = case-sensitive.
|
|
102
|
+
numeric: Character set constraint (0..4).
|
|
103
|
+
calc: 1 if math calculation is required.
|
|
104
|
+
min_len: Minimum character length.
|
|
105
|
+
max_len: Maximum character length.
|
|
106
|
+
language: 0 = default, 1 = Cyrillic, 2 = Latin.
|
|
107
|
+
textinstructions: Instructions for human worker.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Solved text from the image.
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
SolveCaptchaError: If submission fails or times out.
|
|
114
|
+
|
|
115
|
+
Examples:
|
|
116
|
+
>>> client = SolveCaptchaClient(api_key="key")
|
|
117
|
+
>>> text = client.solve_image(body="iVBORw0KGgoAAAANSUhEUgAA...")
|
|
118
|
+
>>> print("Captcha text:", text)
|
|
119
|
+
"""
|
|
120
|
+
params: dict[str, Any] = {"method": "base64", "body": body}
|
|
121
|
+
if phrase is not None:
|
|
122
|
+
params["phrase"] = phrase
|
|
123
|
+
if regsense is not None:
|
|
124
|
+
params["regsense"] = regsense
|
|
125
|
+
if numeric is not None:
|
|
126
|
+
params["numeric"] = numeric
|
|
127
|
+
if calc is not None:
|
|
128
|
+
params["calc"] = calc
|
|
129
|
+
if min_len is not None:
|
|
130
|
+
params["min_len"] = min_len
|
|
131
|
+
if max_len is not None:
|
|
132
|
+
params["max_len"] = max_len
|
|
133
|
+
if language is not None:
|
|
134
|
+
params["language"] = language
|
|
135
|
+
if textinstructions is not None:
|
|
136
|
+
params["textinstructions"] = textinstructions
|
|
137
|
+
|
|
138
|
+
task_id = self.create_task(params)
|
|
139
|
+
return self.wait_for_result(task_id)
|
|
140
|
+
|
|
141
|
+
def solve_text(self, text: str, language: int | None = None) -> str:
|
|
142
|
+
"""Solves a text question captcha.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
text: Question string to solve.
|
|
146
|
+
language: 0 = default, 1 = Cyrillic, 2 = Latin.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Text answer to the question.
|
|
150
|
+
|
|
151
|
+
Examples:
|
|
152
|
+
>>> client = SolveCaptchaClient(api_key="key")
|
|
153
|
+
>>> answer = client.solve_text("If tomorrow is Saturday, what day is today?")
|
|
154
|
+
>>> print("Answer:", answer)
|
|
155
|
+
"""
|
|
156
|
+
params: dict[str, Any] = {"textcaptcha": text}
|
|
157
|
+
if language is not None:
|
|
158
|
+
params["language"] = language
|
|
159
|
+
|
|
160
|
+
task_id = self.create_task(params)
|
|
161
|
+
return self.wait_for_result(task_id)
|
|
162
|
+
|
|
163
|
+
def solve_recaptcha_v2(
|
|
164
|
+
self,
|
|
165
|
+
googlekey: str,
|
|
166
|
+
pageurl: str,
|
|
167
|
+
invisible: bool = False,
|
|
168
|
+
data_s: str | None = None,
|
|
169
|
+
) -> str:
|
|
170
|
+
"""Solves Google reCAPTCHA V2.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
googlekey: Sitekey extracted from target page.
|
|
174
|
+
pageurl: Full URL of the page with the reCAPTCHA.
|
|
175
|
+
invisible: True if widget is invisible.
|
|
176
|
+
data_s: Optional 'data-s' parameter value.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
reCAPTCHA token solution string.
|
|
180
|
+
|
|
181
|
+
Examples:
|
|
182
|
+
>>> client = SolveCaptchaClient(api_key="key")
|
|
183
|
+
>>> token = client.solve_recaptcha_v2(
|
|
184
|
+
... googlekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
|
|
185
|
+
... pageurl="https://example.com/login",
|
|
186
|
+
... )
|
|
187
|
+
"""
|
|
188
|
+
params: dict[str, Any] = {
|
|
189
|
+
"method": "userrecaptcha",
|
|
190
|
+
"googlekey": googlekey,
|
|
191
|
+
"pageurl": pageurl,
|
|
192
|
+
}
|
|
193
|
+
if invisible:
|
|
194
|
+
params["invisible"] = 1
|
|
195
|
+
if data_s:
|
|
196
|
+
params["data-s"] = data_s
|
|
197
|
+
|
|
198
|
+
task_id = self.create_task(params)
|
|
199
|
+
return self.wait_for_result(task_id)
|
|
200
|
+
|
|
201
|
+
def solve_recaptcha_v3(
|
|
202
|
+
self,
|
|
203
|
+
googlekey: str,
|
|
204
|
+
pageurl: str,
|
|
205
|
+
action: str | None = None,
|
|
206
|
+
min_score: float | None = None,
|
|
207
|
+
) -> str:
|
|
208
|
+
"""Solves Google reCAPTCHA V3.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
googlekey: Sitekey extracted from target page.
|
|
212
|
+
pageurl: Full URL of the page.
|
|
213
|
+
action: Action parameter string.
|
|
214
|
+
min_score: Minimum required score threshold (e.g. 0.7).
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
reCAPTCHA token solution string.
|
|
218
|
+
|
|
219
|
+
Examples:
|
|
220
|
+
>>> client = SolveCaptchaClient(api_key="key")
|
|
221
|
+
>>> token = client.solve_recaptcha_v3(
|
|
222
|
+
... googlekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
|
|
223
|
+
... pageurl="https://example.com/page",
|
|
224
|
+
... action="verify",
|
|
225
|
+
... )
|
|
226
|
+
"""
|
|
227
|
+
params: dict[str, Any] = {
|
|
228
|
+
"method": "userrecaptcha",
|
|
229
|
+
"version": "v3",
|
|
230
|
+
"googlekey": googlekey,
|
|
231
|
+
"pageurl": pageurl,
|
|
232
|
+
}
|
|
233
|
+
if action:
|
|
234
|
+
params["action"] = action
|
|
235
|
+
if min_score is not None:
|
|
236
|
+
params["min_score"] = min_score
|
|
237
|
+
|
|
238
|
+
task_id = self.create_task(params)
|
|
239
|
+
return self.wait_for_result(task_id)
|
|
240
|
+
|
|
241
|
+
def solve_turnstile(
|
|
242
|
+
self,
|
|
243
|
+
sitekey: str,
|
|
244
|
+
pageurl: str,
|
|
245
|
+
action: str | None = None,
|
|
246
|
+
data: str | None = None,
|
|
247
|
+
pagedata: str | None = None,
|
|
248
|
+
) -> str:
|
|
249
|
+
"""Solves Cloudflare Turnstile captcha.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
sitekey: Turnstile sitekey.
|
|
253
|
+
pageurl: URL of the protected page.
|
|
254
|
+
action: Optional action parameter.
|
|
255
|
+
data: Optional cData parameter.
|
|
256
|
+
pagedata: Optional pagedata parameter.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Turnstile token solution string.
|
|
260
|
+
|
|
261
|
+
Examples:
|
|
262
|
+
>>> client = SolveCaptchaClient(api_key="key")
|
|
263
|
+
>>> token = client.solve_turnstile(
|
|
264
|
+
... sitekey="0x4AAAAAAABnEBq1w7gYv_m-",
|
|
265
|
+
... pageurl="https://example.com",
|
|
266
|
+
... )
|
|
267
|
+
"""
|
|
268
|
+
params: dict[str, Any] = {
|
|
269
|
+
"method": "turnstile",
|
|
270
|
+
"sitekey": sitekey,
|
|
271
|
+
"pageurl": pageurl,
|
|
272
|
+
}
|
|
273
|
+
if action:
|
|
274
|
+
params["action"] = action
|
|
275
|
+
if data:
|
|
276
|
+
params["data"] = data
|
|
277
|
+
if pagedata:
|
|
278
|
+
params["pagedata"] = pagedata
|
|
279
|
+
|
|
280
|
+
task_id = self.create_task(params)
|
|
281
|
+
return self.wait_for_result(task_id)
|
|
282
|
+
|
|
283
|
+
def solve_funcaptcha(
|
|
284
|
+
self,
|
|
285
|
+
publickey: str,
|
|
286
|
+
pageurl: str,
|
|
287
|
+
surl: str | None = None,
|
|
288
|
+
data: dict[str, Any] | str | None = None,
|
|
289
|
+
) -> str:
|
|
290
|
+
"""Solves Arkose Labs FunCaptcha.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
publickey: Public key of the widget.
|
|
294
|
+
pageurl: URL containing the FunCaptcha.
|
|
295
|
+
surl: Custom service URL.
|
|
296
|
+
data: Additional data payload.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
FunCaptcha solution token.
|
|
300
|
+
|
|
301
|
+
Examples:
|
|
302
|
+
>>> client = SolveCaptchaClient(api_key="key")
|
|
303
|
+
>>> token = client.solve_funcaptcha(
|
|
304
|
+
... publickey="69A21A01-CC43-44CC-A7DC-3E088835E78D",
|
|
305
|
+
... pageurl="https://example.com/signup",
|
|
306
|
+
... )
|
|
307
|
+
"""
|
|
308
|
+
params: dict[str, Any] = {
|
|
309
|
+
"method": "funcaptcha",
|
|
310
|
+
"publickey": publickey,
|
|
311
|
+
"pageurl": pageurl,
|
|
312
|
+
}
|
|
313
|
+
if surl:
|
|
314
|
+
params["surl"] = surl
|
|
315
|
+
if data:
|
|
316
|
+
params["data"] = json.dumps(data) if isinstance(data, dict) else str(data)
|
|
317
|
+
|
|
318
|
+
task_id = self.create_task(params)
|
|
319
|
+
return self.wait_for_result(task_id)
|
|
320
|
+
|
|
321
|
+
def solve_geetest(
|
|
322
|
+
self,
|
|
323
|
+
gt: str,
|
|
324
|
+
challenge: str,
|
|
325
|
+
pageurl: str,
|
|
326
|
+
api_server: str | None = None,
|
|
327
|
+
) -> GeeTestResult:
|
|
328
|
+
"""Solves GeeTest captcha.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
gt: GeeTest 'gt' token.
|
|
332
|
+
challenge: GeeTest 'challenge' token.
|
|
333
|
+
pageurl: URL of the page containing GeeTest.
|
|
334
|
+
api_server: Optional custom api_server domain.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
GeeTestResult with challenge, validate, and seccode.
|
|
338
|
+
|
|
339
|
+
Examples:
|
|
340
|
+
>>> client = SolveCaptchaClient(api_key="key")
|
|
341
|
+
>>> res = client.solve_geetest(
|
|
342
|
+
... gt="022397c99c9d64352069eab73bc373d7",
|
|
343
|
+
... challenge="3049170ffb08bb6b907572ca35489f07",
|
|
344
|
+
... pageurl="https://example.com/geetest",
|
|
345
|
+
... )
|
|
346
|
+
>>> print(res.validate)
|
|
347
|
+
"""
|
|
348
|
+
params: dict[str, Any] = {
|
|
349
|
+
"method": "geetest",
|
|
350
|
+
"gt": gt,
|
|
351
|
+
"challenge": challenge,
|
|
352
|
+
"pageurl": pageurl,
|
|
353
|
+
}
|
|
354
|
+
if api_server:
|
|
355
|
+
params["api_server"] = api_server
|
|
356
|
+
|
|
357
|
+
task_id = self.create_task(params)
|
|
358
|
+
raw = self.wait_for_result(task_id)
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
parsed = json.loads(raw)
|
|
362
|
+
return GeeTestResult(
|
|
363
|
+
challenge=parsed["challenge"],
|
|
364
|
+
validate=parsed["validate"],
|
|
365
|
+
seccode=parsed["seccode"],
|
|
366
|
+
)
|
|
367
|
+
except (json.JSONDecodeError, KeyError) as exc:
|
|
368
|
+
raise SolveCaptchaError(
|
|
369
|
+
f"Failed to parse GeeTest solution: {raw}", "PARSE_ERROR", raw
|
|
370
|
+
) from exc
|
|
371
|
+
|
|
372
|
+
def create_task(self, params: dict[str, Any]) -> str:
|
|
373
|
+
"""Submits a captcha solving task to /in.php.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
params: Parameters specifying captcha type and payload.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Task ID string.
|
|
380
|
+
|
|
381
|
+
Raises:
|
|
382
|
+
SolveCaptchaError: If task submission is rejected.
|
|
383
|
+
"""
|
|
384
|
+
payload: dict[str, Any] = {
|
|
385
|
+
"key": self.api_key,
|
|
386
|
+
"json": 1,
|
|
387
|
+
**params,
|
|
388
|
+
}
|
|
389
|
+
if self.soft_id and "soft_id" not in payload:
|
|
390
|
+
payload["soft_id"] = self.soft_id
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
resp = self._client.post("/in.php", params=payload)
|
|
394
|
+
resp.raise_for_status()
|
|
395
|
+
data = resp.json()
|
|
396
|
+
|
|
397
|
+
if isinstance(data, dict):
|
|
398
|
+
if data.get("status") == 1 and data.get("request"):
|
|
399
|
+
return str(data["request"])
|
|
400
|
+
if data.get("status") == 0 and data.get("request"):
|
|
401
|
+
code = str(data["request"])
|
|
402
|
+
desc = ERROR_DESCRIPTIONS.get(code, f"SolveCaptcha error: {code}")
|
|
403
|
+
raise SolveCaptchaError(desc, code=code, response_body=data)
|
|
404
|
+
|
|
405
|
+
raise SolveCaptchaError(
|
|
406
|
+
f"Unexpected /in.php response: {resp.text}", "INVALID_RESPONSE", data
|
|
407
|
+
)
|
|
408
|
+
except httpx.HTTPError as exc:
|
|
409
|
+
raise SolveCaptchaError(f"create_task request failed: {exc}", "NETWORK_ERROR") from exc
|
|
410
|
+
|
|
411
|
+
def get_task_result(self, task_id: str) -> str | None:
|
|
412
|
+
"""Checks the resolution status of an active task.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
task_id: Task ID returned by create_task.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Solution string or None if still processing.
|
|
419
|
+
"""
|
|
420
|
+
try:
|
|
421
|
+
return self._query_result({"action": "get", "id": task_id})
|
|
422
|
+
except SolveCaptchaError as exc:
|
|
423
|
+
if exc.code == "CAPCHA_NOT_READY":
|
|
424
|
+
return None
|
|
425
|
+
raise
|
|
426
|
+
|
|
427
|
+
def wait_for_result(
|
|
428
|
+
self,
|
|
429
|
+
task_id: str,
|
|
430
|
+
timeout: float | None = None,
|
|
431
|
+
interval: float | None = None,
|
|
432
|
+
) -> str:
|
|
433
|
+
"""Polls until a task is solved or timeout occurs.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
task_id: Task ID.
|
|
437
|
+
timeout: Maximum seconds to wait.
|
|
438
|
+
interval: Polling frequency in seconds.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Solved captcha response string.
|
|
442
|
+
|
|
443
|
+
Raises:
|
|
444
|
+
SolveCaptchaError: If timed out or task encounters an error.
|
|
445
|
+
"""
|
|
446
|
+
effective_timeout = timeout if timeout is not None else self.polling_timeout
|
|
447
|
+
effective_interval = interval if interval is not None else self.polling_interval
|
|
448
|
+
start = time.time()
|
|
449
|
+
|
|
450
|
+
while time.time() - start < effective_timeout:
|
|
451
|
+
res = self.get_task_result(task_id)
|
|
452
|
+
if res is not None:
|
|
453
|
+
return res
|
|
454
|
+
time.sleep(effective_interval)
|
|
455
|
+
|
|
456
|
+
raise SolveCaptchaError(
|
|
457
|
+
f"Captcha solving timed out after {effective_timeout}s",
|
|
458
|
+
"TIMEOUT",
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
def report_good(self, task_id: str) -> bool:
|
|
462
|
+
"""Reports a correctly solved captcha.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
task_id: Task ID.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
True if report was recorded.
|
|
469
|
+
"""
|
|
470
|
+
try:
|
|
471
|
+
res = self._query_result({"action": "reportgood", "id": task_id})
|
|
472
|
+
return res == "OK_REPORT_RECORDED"
|
|
473
|
+
except SolveCaptchaError:
|
|
474
|
+
return False
|
|
475
|
+
|
|
476
|
+
def report_bad(self, task_id: str) -> bool:
|
|
477
|
+
"""Reports an incorrectly solved captcha.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
task_id: Task ID.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
True if report was recorded.
|
|
484
|
+
"""
|
|
485
|
+
try:
|
|
486
|
+
res = self._query_result({"action": "reportbad", "id": task_id})
|
|
487
|
+
return res == "OK_REPORT_RECORDED"
|
|
488
|
+
except SolveCaptchaError:
|
|
489
|
+
return False
|
|
490
|
+
|
|
491
|
+
def _query_result(self, params: dict[str, Any]) -> str:
|
|
492
|
+
query = {"key": self.api_key, "json": 1, **params}
|
|
493
|
+
try:
|
|
494
|
+
resp = self._client.get("/res.php", params=query)
|
|
495
|
+
resp.raise_for_status()
|
|
496
|
+
data = resp.json()
|
|
497
|
+
|
|
498
|
+
if isinstance(data, dict):
|
|
499
|
+
if data.get("status") == 1 and data.get("request") is not None:
|
|
500
|
+
return str(data["request"])
|
|
501
|
+
if data.get("status") == 0 and data.get("request"):
|
|
502
|
+
code = str(data["request"])
|
|
503
|
+
desc = ERROR_DESCRIPTIONS.get(code, f"SolveCaptcha error: {code}")
|
|
504
|
+
raise SolveCaptchaError(desc, code=code, response_body=data)
|
|
505
|
+
|
|
506
|
+
raise SolveCaptchaError(
|
|
507
|
+
f"Unexpected /res.php response: {resp.text}", "INVALID_RESPONSE", data
|
|
508
|
+
)
|
|
509
|
+
except httpx.HTTPError as exc:
|
|
510
|
+
raise SolveCaptchaError(f"HTTP request failed: {exc}", "NETWORK_ERROR") from exc
|
solvecaptcha/errors.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Error definitions for the SolveCaptcha client."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
ERROR_DESCRIPTIONS: dict[str, str] = {
|
|
6
|
+
"ERROR_WRONG_USER_KEY": "API key is invalid or formatted incorrectly",
|
|
7
|
+
"ERROR_KEY_DOES_NOT_EXIST": "API key not found in SolveCaptcha system",
|
|
8
|
+
"ERROR_ZERO_BALANCE": "Account balance is zero or depleted",
|
|
9
|
+
"ERROR_NO_SLOT_AVAILABLE": "No idle workers available at the moment",
|
|
10
|
+
"ERROR_ZERO_CAPTCHA_FILESIZE": "Uploaded image file size is 0 bytes",
|
|
11
|
+
"ERROR_TOO_BIG_CAPTCHA_FILESIZE": "Uploaded image exceeds maximum size limit (100 KB)",
|
|
12
|
+
"ERROR_WRONG_FILE_EXTENSION": "File extension is not supported",
|
|
13
|
+
"ERROR_IMAGE_TYPE_NOT_SUPPORTED": "Image format cannot be loaded or is corrupted",
|
|
14
|
+
"ERROR_IP_NOT_ALLOWED": "Your IP address is not whitelisted or is blocked",
|
|
15
|
+
"IP_BANNED": "Your IP address has been temporarily banned due to excessive failed attempts",
|
|
16
|
+
"ERROR_CAPTCHA_UNSOLVABLE": "Worker could not solve the submitted captcha",
|
|
17
|
+
"ERROR_BAD_DUPLICATES": "Maximum duplicate requests limit reached",
|
|
18
|
+
"REPORT_NOT_RECORDED": "Feedback report could not be recorded",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SolveCaptchaError(Exception):
|
|
23
|
+
"""Exception raised when a SolveCaptcha API request or task fails."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
message: str,
|
|
28
|
+
code: str | None = None,
|
|
29
|
+
response_body: Any | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
super().__init__(message)
|
|
32
|
+
self.code = code
|
|
33
|
+
self.response_body = response_body
|
solvecaptcha/types.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Data types and models for the SolveCaptcha API."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class GeeTestResult:
|
|
8
|
+
"""Result for GeeTest captcha solutions."""
|
|
9
|
+
|
|
10
|
+
challenge: str
|
|
11
|
+
validate: str
|
|
12
|
+
seccode: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class CaptchaTask:
|
|
17
|
+
"""Represents an ongoing captcha solving task."""
|
|
18
|
+
|
|
19
|
+
task_id: str
|
|
20
|
+
solution: str | None = None
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: solvecaptcha
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the SolveCaptcha human and AI captcha solving API
|
|
5
|
+
Project-URL: Repository, https://github.com/TysikGG/api-kit
|
|
6
|
+
Project-URL: Documentation, https://solvecaptcha.com/captcha-solver-api
|
|
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
|
+
# solvecaptcha
|
|
14
|
+
|
|
15
|
+
[](https://pypi.org/project/solvecaptcha/)
|
|
16
|
+
[](LICENSE)
|
|
17
|
+
[](https://pypi.org/project/solvecaptcha/)
|
|
18
|
+
|
|
19
|
+
Modern, fully typed Python client library for the [SolveCaptcha](https://solvecaptcha.com/captcha-solver-api) recognition API.
|
|
20
|
+
|
|
21
|
+
Official API documentation: <https://solvecaptcha.com/captcha-solver-api>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- ⚡ **Multi-Captcha Support** — Cloudflare Turnstile, Google reCAPTCHA v2 / v3, Arkose FunCaptcha, GeeTest v3, normal image captchas, and text questions.
|
|
28
|
+
- 🔄 **Built-in Auto Polling** — High-level methods like `solve_turnstile()` and `solve_recaptcha_v2()` automatically poll until resolved.
|
|
29
|
+
- 🐍 **Modern Pythonic Design** — Type annotations, dataclasses, context manager support (`with SolveCaptchaClient(...) as client:`), and custom exceptions.
|
|
30
|
+
- 🛠️ **Asynchronous Task Queue** — Low-level `create_task()` and `get_task_result()` for custom background queue workers.
|
|
31
|
+
- ⚖️ **Accuracy Reports** — `report_good()` and `report_bad()` for solving quality feedback and automated credit refunds.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install solvecaptcha
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python 3.10 or higher.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Quick Start (Safe Check)
|
|
46
|
+
|
|
47
|
+
The quick check only queries your account balance. It does not create tasks or consume credits.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import os
|
|
51
|
+
from solvecaptcha import SolveCaptchaClient
|
|
52
|
+
|
|
53
|
+
client = SolveCaptchaClient(api_key=os.environ["SOLVECAPTCHA_API_KEY"])
|
|
54
|
+
|
|
55
|
+
balance = client.get_balance()
|
|
56
|
+
print(f"Current SolveCaptcha balance: ${balance:.4f} USD")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Usage Guide
|
|
62
|
+
|
|
63
|
+
### 1. Solving Cloudflare Turnstile
|
|
64
|
+
|
|
65
|
+
Solve Cloudflare Turnstile challenges and retrieve the verification token:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from solvecaptcha import SolveCaptchaClient
|
|
69
|
+
|
|
70
|
+
with SolveCaptchaClient(api_key="your_api_key") as client:
|
|
71
|
+
token = client.solve_turnstile(
|
|
72
|
+
sitekey="0x4AAAAAAADnPIDROrmt1Wwj",
|
|
73
|
+
pageurl="https://example.com/protected",
|
|
74
|
+
action="login", # optional
|
|
75
|
+
)
|
|
76
|
+
print("Turnstile token:", token)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 2. Solving Google reCAPTCHA v2 & v3
|
|
80
|
+
|
|
81
|
+
Solve checkbox or invisible reCAPTCHA v2, or score-based reCAPTCHA v3:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
with SolveCaptchaClient(api_key="your_api_key") as client:
|
|
85
|
+
# reCAPTCHA v2
|
|
86
|
+
token_v2 = client.solve_recaptcha_v2(
|
|
87
|
+
googlekey="6Le-wvkSAAAAAPBMRTvw0Q4CeHg-tUnSQVwyYcxQ",
|
|
88
|
+
pageurl="https://example.com/protected",
|
|
89
|
+
invisible=0,
|
|
90
|
+
)
|
|
91
|
+
print("reCAPTCHA v2 token:", token_v2)
|
|
92
|
+
|
|
93
|
+
# reCAPTCHA v3
|
|
94
|
+
token_v3 = client.solve_recaptcha_v3(
|
|
95
|
+
googlekey="6LdyC2cUAAAAABlzY2Hg-tUnSQVwyYcxQ",
|
|
96
|
+
pageurl="https://example.com/checkout",
|
|
97
|
+
action="checkout",
|
|
98
|
+
min_score=0.7,
|
|
99
|
+
)
|
|
100
|
+
print("reCAPTCHA v3 token:", token_v3)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 3. Solving Normal Image CAPTCHA
|
|
104
|
+
|
|
105
|
+
Solve distorted image captchas by passing a base64-encoded string:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import base64
|
|
109
|
+
from solvecaptcha import SolveCaptchaClient
|
|
110
|
+
|
|
111
|
+
with open("captcha.png", "rb") as f:
|
|
112
|
+
b64_image = base64.b64encode(f.read()).decode("utf-8")
|
|
113
|
+
|
|
114
|
+
with SolveCaptchaClient(api_key="your_api_key") as client:
|
|
115
|
+
solution_text = client.solve_image(
|
|
116
|
+
body=b64_image,
|
|
117
|
+
numeric=1, # numbers only
|
|
118
|
+
min_len=4,
|
|
119
|
+
max_len=6,
|
|
120
|
+
)
|
|
121
|
+
print("Solved captcha text:", solution_text)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 4. Low-Level Task Flow
|
|
125
|
+
|
|
126
|
+
For asynchronous background tasks or task queues:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import time
|
|
130
|
+
from solvecaptcha import SolveCaptchaClient
|
|
131
|
+
|
|
132
|
+
with SolveCaptchaClient(api_key="your_api_key") as client:
|
|
133
|
+
# 1. Create task
|
|
134
|
+
task_id = client.create_task({
|
|
135
|
+
"method": "turnstile",
|
|
136
|
+
"sitekey": "0x4AAAAAAADnPIDROrmt1Wwj",
|
|
137
|
+
"pageurl": "https://example.com",
|
|
138
|
+
})
|
|
139
|
+
print(f"Task submitted: {task_id}")
|
|
140
|
+
|
|
141
|
+
# 2. Poll manually or wait
|
|
142
|
+
token = None
|
|
143
|
+
for _ in range(30):
|
|
144
|
+
time.sleep(5)
|
|
145
|
+
token = client.get_task_result(task_id)
|
|
146
|
+
if token:
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
if token:
|
|
150
|
+
print("Solved:", token)
|
|
151
|
+
client.report_good(task_id)
|
|
152
|
+
else:
|
|
153
|
+
print("Failed to resolve in time.")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## API Reference
|
|
159
|
+
|
|
160
|
+
### `SolveCaptchaClient`
|
|
161
|
+
|
|
162
|
+
#### Constructor
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
SolveCaptchaClient(
|
|
166
|
+
api_key: str,
|
|
167
|
+
base_url: str = "https://solvecaptcha.com",
|
|
168
|
+
timeout: float = 30.0,
|
|
169
|
+
default_polling_timeout: float = 120.0,
|
|
170
|
+
default_polling_interval: float = 5.0,
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
| Parameter | Type | Default | Description |
|
|
175
|
+
| :--- | :--- | :--- | :--- |
|
|
176
|
+
| `api_key` | `str` | **Required** | Your SolveCaptcha API key. |
|
|
177
|
+
| `base_url` | `str` | `"https://solvecaptcha.com"` | Custom API base URL. |
|
|
178
|
+
| `timeout` | `float` | `30.0` | HTTP request timeout in seconds. |
|
|
179
|
+
| `default_polling_timeout` | `float` | `120.0` | Max seconds to wait for resolution. |
|
|
180
|
+
| `default_polling_interval` | `float` | `5.0` | Seconds between status checks. |
|
|
181
|
+
|
|
182
|
+
#### Methods
|
|
183
|
+
|
|
184
|
+
- **`get_balance() -> float`** — Returns current balance in USD.
|
|
185
|
+
- **`solve_turnstile(sitekey, pageurl, action=None, data=None) -> str`** — Solves Cloudflare Turnstile.
|
|
186
|
+
- **`solve_recaptcha_v2(googlekey, pageurl, invisible=0, data_s=None, enterprise=0) -> str`** — Solves reCAPTCHA v2.
|
|
187
|
+
- **`solve_recaptcha_v3(googlekey, pageurl, action=None, min_score=None) -> str`** — Solves reCAPTCHA v3.
|
|
188
|
+
- **`solve_funcaptcha(publickey, pageurl, surl=None, data=None) -> str`** — Solves Arkose FunCaptcha.
|
|
189
|
+
- **`solve_geetest(gt, challenge, pageurl, api_server=None) -> GeeTestResult`** — Solves GeeTest v3.
|
|
190
|
+
- **`solve_image(body, phrase=0, regsense=0, numeric=0, calc=0, min_len=None, max_len=None, language=None, textinstructions=None) -> str`** — Solves base64 image.
|
|
191
|
+
- **`solve_text(text, language=None) -> str`** — Solves text question captcha.
|
|
192
|
+
- **`create_task(params: dict) -> str`** — Sends raw task payload to `/in.php` and returns task ID.
|
|
193
|
+
- **`get_task_result(task_id: str) -> str | None`** — Checks task status, returns token or `None` if not ready.
|
|
194
|
+
- **`wait_for_result(task_id: str, timeout_sec=None, interval_sec=None) -> str`** — Polls until solved.
|
|
195
|
+
- **`report_good(task_id: str) -> bool`** — Marks solution as correct.
|
|
196
|
+
- **`report_bad(task_id: str) -> bool`** — Marks solution as incorrect to receive a credit refund.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Error Handling
|
|
201
|
+
|
|
202
|
+
When an API error occurs, `SolveCaptchaError` is raised:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from solvecaptcha import SolveCaptchaClient, SolveCaptchaError
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
with SolveCaptchaClient(api_key="invalid_key") as client:
|
|
209
|
+
client.get_balance()
|
|
210
|
+
except SolveCaptchaError as exc:
|
|
211
|
+
print(f"Error Code: {exc.code}")
|
|
212
|
+
print(f"Message: {exc.message}")
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Common error codes:
|
|
216
|
+
- `ERROR_WRONG_USER_KEY` — Provided API key is incorrect.
|
|
217
|
+
- `ERROR_KEY_DOES_NOT_EXIST` — Invalid API key.
|
|
218
|
+
- `ERROR_ZERO_BALANCE` — Insufficient funds on account.
|
|
219
|
+
- `ERROR_NO_SLOT_AVAILABLE` — Solving queue currently congested.
|
|
220
|
+
- `CAPCHA_NOT_READY` — Resolution in progress.
|
|
221
|
+
- `TIMEOUT` — Maximum polling wait duration exceeded.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
ISC
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
solvecaptcha/__init__.py,sha256=tgVLTlnkBP5leBgX5iGlAGioSYxsNOcjq_4djncignA,304
|
|
2
|
+
solvecaptcha/client.py,sha256=lwV7afPPPlf8Y7Gd6Ew0s0zwaB2WhvpT6Ui0-ce82lY,16777
|
|
3
|
+
solvecaptcha/errors.py,sha256=Rj8HOcsZ2ji-P4k7x0sD_b3pwfWdVhSLqgiU7BKTGds,1487
|
|
4
|
+
solvecaptcha/types.py,sha256=Eyb85NxxMISk0IGIGK8UOYphL-TbNXcrPdSEtYpViTE,362
|
|
5
|
+
solvecaptcha-0.1.0.dist-info/METADATA,sha256=PhGqFOX3pPihwi20gFh2jvgnPZJW7pY4ZgaybEqgyQY,7400
|
|
6
|
+
solvecaptcha-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
7
|
+
solvecaptcha-0.1.0.dist-info/licenses/LICENSE,sha256=8S0uYmn_aeljwFudfBoV91HcIo79yab9s0yhMVsOHJ0,751
|
|
8
|
+
solvecaptcha-0.1.0.dist-info/RECORD,,
|
|
@@ -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.
|