diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 16e814f..46b1740 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -38,7 +38,10 @@ jobs:
# to catch packaging errors early. Skipped if lint fails to save CI minutes.
test:
needs: lint
- runs-on: ubuntu-latest
+ # Ubuntu 26.04 (the current ubuntu-latest) does not provide Python 3.9
+ # through actions/setup-python. Pin the matrix runner while Python 3.9 is
+ # part of the SDK's supported version range.
+ runs-on: ubuntu-24.04
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
diff --git a/README.md b/README.md
index 8695653..e23adae 100644
--- a/README.md
+++ b/README.md
@@ -499,11 +499,9 @@ With proxy, use `GeeTestTask` (same proxy fields as reCAPTCHA v2).
[API method description.](https://captcha-solver.com/en/docs/captcha-types#yandex-smartcaptcha)
Use this method to solve the token-based Yandex SmartCaptcha and obtain a token
-for the widget on the target page. Use the coordinates method for image challenges.
+for the widget on the target page. Use the dedicated image method for image challenges.
-`YandexSmartCaptchaTaskProxyless` / `YandexSmartCaptchaTask` -- token-based
-challenge. For the image challenge instead, use `CoordinatesTask` with
-[Coordinates](#coordinates-click-captcha) is available for generic click captchas.
+`YandexSmartCaptchaTaskProxyless` / `YandexSmartCaptchaTask` -- token-based challenge.
| Parameter | Required | Description |
|---|---|---|
@@ -532,15 +530,45 @@ print(result["token"])
With proxy, use `YandexSmartCaptchaTask` (same proxy fields as reCAPTCHA v2,
plus the `https` option above).
+#### Image challenge
+
+Use `YandexSmartCaptchaImageTask` when SmartCaptcha shows a main image and a
+separate strip indicating the symbols to click. The SDK sends API v2 type
+`SmartCaptchaTask`; both images must be base64-encoded.
+
+| Parameter | Required | Description |
+|---|---|---|
+| `image` | yes | Main captcha image, base64-encoded. |
+| `imgInstructions` | yes | Click-order instruction image, base64-encoded. |
+| `comment` | no | Optional text hint for the worker. |
+
+**Response:** `coordinates` -- ordered `{"x": int, "y": int}` click positions.
+
+```python
+import base64
+from captcha_solver_api import CaptchaClient
+from captcha_solver_api.tasks import YandexSmartCaptchaImageTask
+
+with open("captcha.png", "rb") as f:
+ image = base64.b64encode(f.read()).decode("utf-8")
+with open("instructions.png", "rb") as f:
+ instructions = base64.b64encode(f.read()).decode("utf-8")
+
+task = YandexSmartCaptchaImageTask(
+ image=image,
+ imgInstructions=instructions,
+)
+result = CaptchaClient("your_api_key").solve(task)
+print(result["coordinates"])
+```
+
### Coordinates (click captcha)
[API method description.](https://captcha-solver.com/en/docs/captcha-types#coordinates)
-Use this method to identify points that a worker should click in an image. It
-supports generic click captchas and the image version of Yandex SmartCaptcha.
+Use this method to identify points that a worker should click in a generic image.
-`CoordinatesTask`. Used both for generic "click on X" captchas and for Yandex
-SmartCaptcha's image challenge. No proxy variant -- the image is submitted directly.
+`CoordinatesTask`. No proxy variant -- the image is submitted directly.
| Parameter | Required | Description |
|---|---|---|
@@ -718,7 +746,8 @@ Avoid importing it by name: it shadows Python's built-in `TimeoutError`.
See the dedicated [examples documentation](https://github.com/captcha-solver-api/python-sdk/blob/main/examples/README.md) for the full
sync/async example list, setup steps, expected results, and placeholder guidance.
-- **Image/click captchas** (`image_to_text.py`, `coordinates.py`) run after installing the dependencies and setting a valid
+- **Image/click captchas** (`image_to_text.py`, `coordinates.py`,
+ `yandex_smartcaptcha_image.py`) run after installing the dependencies and setting a valid
`CAPTCHA_API_KEY` -- they read sample images bundled in
[examples/assets](https://github.com/captcha-solver-api/python-sdk/tree/main/examples/assets), no target page needed.
`python-dotenv` is optional: install it only if you want the scripts to load a `.env` file.
@@ -743,6 +772,7 @@ export CAPTCHA_API_KEY=your_api_key
python examples/sync/balance.py
python examples/sync/image_to_text.py
python examples/sync/coordinates.py
+python examples/sync/yandex_smartcaptcha_image.py
```
Live checks with real parameters have exercised all supported task types.
diff --git a/captcha_solver_api/tasks.py b/captcha_solver_api/tasks.py
index 5df677e..0f7a500 100644
--- a/captcha_solver_api/tasks.py
+++ b/captcha_solver_api/tasks.py
@@ -787,7 +787,7 @@ def __init__(
class CoordinatesTask(BaseTask):
"""Coordinate-based (click) image captcha. No proxy variant -- the image
- is submitted directly.
+ is submitted directly. Use this task for generic click captchas.
Args:
body: The captcha image, base64-encoded (no `data:image/...;base64,` prefix).
@@ -832,6 +832,31 @@ def __init__(
self.maxClicks = maxClicks
+class YandexSmartCaptchaImageTask(BaseTask):
+ """Image-based Yandex SmartCaptcha using API v2 ``SmartCaptchaTask``.
+
+ Args:
+ image: Main SmartCaptcha challenge image, base64-encoded.
+ imgInstructions: Click-order instruction image, base64-encoded.
+ comment: Optional text hint for the worker.
+
+ Returns (``solution`` from ``solve()``):
+ ``coordinates`` -- an ordered list of ``{"x": int, "y": int}`` points.
+ """
+
+ type = "SmartCaptchaTask"
+
+ def __init__(
+ self,
+ image: str,
+ imgInstructions: str,
+ comment: Optional[str] = None,
+ ) -> None:
+ self.image = image
+ self.imgInstructions = imgInstructions
+ self.comment = comment
+
+
class TencentTaskProxyless(BaseTask):
"""Tencent captcha without proxy.
diff --git a/examples/README.md b/examples/README.md
index 00057ac..43772f8 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -61,6 +61,7 @@ Each supported CAPTCHA type has a matching sync and async script:
| GeeTest v3 | [geetest_v3.py](sync/geetest_v3.py) | [geetest_v3.py](async/geetest_v3.py) | `challenge`, `validate`, `seccode` |
| GeeTest v4 | [geetest_v4.py](sync/geetest_v4.py) | [geetest_v4.py](async/geetest_v4.py) | `captcha_output` and related fields |
| Yandex SmartCaptcha token | [yandex_smartcaptcha.py](sync/yandex_smartcaptcha.py) | [yandex_smartcaptcha.py](async/yandex_smartcaptcha.py) | `token` |
+| Yandex SmartCaptcha image | [yandex_smartcaptcha_image.py](sync/yandex_smartcaptcha_image.py) | [yandex_smartcaptcha_image.py](async/yandex_smartcaptcha_image.py) | `coordinates` |
| Coordinates / click captcha | [coordinates.py](sync/coordinates.py) | [coordinates.py](async/coordinates.py) | `coordinates` |
| Tencent | [tencent.py](sync/tencent.py) | [tencent.py](async/tencent.py) | `appid`, `ret`, `ticket`, `randstr` |
diff --git a/examples/assets/yandex-smartcaptcha-image.png b/examples/assets/yandex-smartcaptcha-image.png
new file mode 100644
index 0000000..92addc3
Binary files /dev/null and b/examples/assets/yandex-smartcaptcha-image.png differ
diff --git a/examples/assets/yandex-smartcaptcha-instructions.png b/examples/assets/yandex-smartcaptcha-instructions.png
new file mode 100644
index 0000000..80cf090
Binary files /dev/null and b/examples/assets/yandex-smartcaptcha-instructions.png differ
diff --git a/examples/async/yandex_smartcaptcha_image.py b/examples/async/yandex_smartcaptcha_image.py
new file mode 100644
index 0000000..a89202c
--- /dev/null
+++ b/examples/async/yandex_smartcaptcha_image.py
@@ -0,0 +1,54 @@
+"""Example: Solve an image-based Yandex SmartCaptcha asynchronously.
+
+Prerequisites:
+ Set the CAPTCHA_API_KEY environment variable.
+ The sample captcha and instruction images are bundled in examples/assets/.
+"""
+
+import asyncio
+import os
+import sys
+from base64 import b64encode
+
+try:
+ from dotenv import load_dotenv
+except ModuleNotFoundError as exc:
+ if exc.name != "dotenv":
+ raise
+ load_dotenv = None
+
+repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
+sys.path.append(repo_root)
+if load_dotenv is not None:
+ load_dotenv(os.path.join(repo_root, ".env"))
+
+from captcha_solver_api import AsyncCaptchaClient
+from captcha_solver_api.tasks import YandexSmartCaptchaImageTask
+
+
+def encode_image(filename: str) -> str:
+ """Return an image from examples/assets as a Base64 string."""
+ path = os.path.join(repo_root, "examples", "assets", filename)
+ with open(path, "rb") as image_file:
+ return b64encode(image_file.read()).decode("utf-8")
+
+
+async def main() -> None:
+ api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY")
+ async with AsyncCaptchaClient(api_key) as solver:
+ result = await solver.solve(
+ YandexSmartCaptchaImageTask(
+ image=encode_image("yandex-smartcaptcha-image.png"),
+ imgInstructions=encode_image("yandex-smartcaptcha-instructions.png"),
+ )
+ )
+ # Click the returned points on the captcha image in the listed order.
+ # Example: {"coordinates": [{"x": 73, "y": 92}, ...]}
+ print("result: " + str(result))
+
+
+if __name__ == "__main__":
+ try:
+ asyncio.run(main())
+ except Exception as exc:
+ sys.exit(exc)
diff --git a/examples/sync/yandex_smartcaptcha_image.py b/examples/sync/yandex_smartcaptcha_image.py
new file mode 100644
index 0000000..9973cdc
--- /dev/null
+++ b/examples/sync/yandex_smartcaptcha_image.py
@@ -0,0 +1,49 @@
+"""Example: Solve an image-based Yandex SmartCaptcha.
+
+Prerequisites:
+ Set the CAPTCHA_API_KEY environment variable.
+ The sample captcha and instruction images are bundled in examples/assets/.
+"""
+
+import os
+import sys
+from base64 import b64encode
+
+try:
+ from dotenv import load_dotenv
+except ModuleNotFoundError as exc:
+ if exc.name != "dotenv":
+ raise
+ load_dotenv = None
+
+repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
+sys.path.append(repo_root)
+if load_dotenv is not None:
+ load_dotenv(os.path.join(repo_root, ".env"))
+
+from captcha_solver_api import CaptchaClient
+from captcha_solver_api.tasks import YandexSmartCaptchaImageTask
+
+
+def encode_image(filename: str) -> str:
+ """Return an image from examples/assets as a Base64 string."""
+ path = os.path.join(repo_root, "examples", "assets", filename)
+ with open(path, "rb") as image_file:
+ return b64encode(image_file.read()).decode("utf-8")
+
+
+api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY")
+solver = CaptchaClient(api_key)
+
+try:
+ result = solver.solve(
+ YandexSmartCaptchaImageTask(
+ image=encode_image("yandex-smartcaptcha-image.png"),
+ imgInstructions=encode_image("yandex-smartcaptcha-instructions.png"),
+ )
+ )
+ # Click the returned points on the captcha image in the listed order.
+ # Example: {"coordinates": [{"x": 73, "y": 92}, ...]}
+ print("result: " + str(result))
+except Exception as exc:
+ sys.exit(exc)
diff --git a/tests/sync/test_coordinates.py b/tests/sync/test_coordinates.py
index d9509b2..ab26984 100644
--- a/tests/sync/test_coordinates.py
+++ b/tests/sync/test_coordinates.py
@@ -23,11 +23,13 @@ def test_to_dict(self):
def test_with_click_limits(self):
task = CoordinatesTask(
body="base64string",
+ imgInstructions="base64instructions",
minClicks=1,
maxClicks=5,
)
result = task.to_dict()
+ assert result["imgInstructions"] == "base64instructions"
assert result["minClicks"] == 1
assert result["maxClicks"] == 5
diff --git a/tests/sync/test_yandex_smartcaptcha_image.py b/tests/sync/test_yandex_smartcaptcha_image.py
new file mode 100644
index 0000000..7f308ce
--- /dev/null
+++ b/tests/sync/test_yandex_smartcaptcha_image.py
@@ -0,0 +1,23 @@
+"""Tests for YandexSmartCaptchaImageTask."""
+
+from captcha_solver_api.tasks import YandexSmartCaptchaImageTask
+
+
+class TestYandexSmartCaptchaImage:
+ def test_to_dict_uses_api_v2_task_type(self):
+ task = YandexSmartCaptchaImageTask(
+ image="captcha-image",
+ imgInstructions="instruction-image",
+ comment="click the symbols in the specified order",
+ )
+
+ assert task.to_dict() == {
+ "type": "SmartCaptchaTask",
+ "image": "captcha-image",
+ "imgInstructions": "instruction-image",
+ "comment": "click the symbols in the specified order",
+ }
+
+ def test_optional_comment_is_omitted(self):
+ task = YandexSmartCaptchaImageTask("captcha-image", "instruction-image")
+ assert "comment" not in task.to_dict()