# GeeTest: Captcha.fun API

> Solve GeeTest v4 and v3 challenges and submit the result for site-owner verification.

## At a glance

- **`kind`**: `geetest` (Captcha.fun's IPs), or `geetest_proxy` with a `proxy` (your own IP)
- **You send**: `url`, `siteKey` (optional: `geetestChallenge`, `geetestApiServer`, `geetestProduct`, `geetestLang`, `geetestNewCaptcha`, `geetestRiskType`, `geetestUserInfo`)
- **You get**: GeeTest v4 or v3 validate values → `geetest`
- **Default price**: $1.50 per 1,000 successful solves (15 credits per solve). Your API key's rate may differ.
- **Flow**: `POST /v1/request` → `GET /v2/response/{id}` until `ready` (details below)

## Read this first

- v4 (default): `siteKey` is the `captchaId`. Returns four fields for GeeTest’s validate endpoint.
- v3: `siteKey` is the `gt`, and `geetestChallenge` is the challenge from your own register call. Returns `geetest_challenge`, `geetest_validate`, and `geetest_seccode`.
- Optional: `geetestApiServer` for self-hosted v3, `geetestRiskType` for gated v4, and `geetestUserInfo` when v4 is bound to the site’s session.

## Request: `POST /v1/request`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | string | yes | `geetest` · `geetest_proxy` |
| `url` | string | yes | Page URL where the challenge appears. |
| `siteKey` | string | yes | Site key of the challenge. |
| `geetestChallenge` | string | no | Required for v3. Challenge from your own register call; setting it switches the solve to v3. Challenges are single-use and short-lived — mint one fresh challenge per solve attempt and never retry a failed attempt with the same challenge |
| `geetestApiServer` | string | no | v3 only. Self-hosted API host. Omit for GeeTest’s servers |
| `geetestProduct` | string | no | Widget mode: `bind` (default), `popup`, or `float` |
| `geetestLang` | string | no | Widget language. Defaults to `eng` / `en` |
| `geetestNewCaptcha` | string | no | v3 only. `true` or `1` sends `new_captcha` on init |
| `geetestRiskType` | string | no | Gated v4 sites only. Site-issued risk token; without it `/load` fails with `illegal risk_type` |
| `geetestUserInfo` | string | no | v4 only. Site-issued `userInfo` JSON, e.g. `{"session_id":"..."}`, passed through as-is |
| `proxy` | string | no | For `_proxy` kinds only: your proxy as `host:port` or `host:port:username:password`. Ignored by non-proxy kinds. If a `_proxy` kind is sent with no proxy, Captcha.fun's own IPs are used. |
| `platform` | string | no | Requested platform label: `windows` (default) or `mac`. |

Headers: `API_KEY: <key>`, `Content-Type: application/json`; optional `APP_ID` to tag requests.

Example body:

```json
{
  "kind": "geetest",
  "url": "https://your-domain.example/login",
  "siteKey": "YOUR_GEETEST_CAPTCHA_ID"
}
```

## End-to-end example (Python)

Install `requests` (`python -m pip install requests`) and set `CAPTCHAFUN_API_KEY` in your environment. Replace the example URL, keys, and challenge inputs with your own values.

`poll_budget` limits polling and waits between requests. Requests' `timeout` limits connection/read inactivity, not total elapsed time; a slow streaming response can exceed the polling budget.

```python
import os, time, requests
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime

API = "https://api.captcha.fun"
HEADERS = {"API_KEY": os.environ["CAPTCHAFUN_API_KEY"]}  # key looks like CAPFUN-XXXXXXXX...; never hardcode it


def solve(body, poll_budget=120):
    """Create once, then check the time budget between result reads and waits."""
    deadline = time.monotonic() + poll_budget
    solve_id = None

    def remaining():
        seconds = deadline - time.monotonic()
        if seconds <= 0:
            raise TimeoutError(f"Polling budget exhausted (solve ID: {solve_id}); an accepted solve may still be running")
        return seconds

    def wait(seconds):
        time.sleep(min(seconds, remaining()))

    def retry_after(response, fallback):
        value = response.headers.get("Retry-After", "").strip()
        try:
            return max(1, int(value))
        except ValueError:
            pass
        try:
            when = parsedate_to_datetime(value)
            if when.tzinfo is None:
                when = when.replace(tzinfo=timezone.utc)
            return max(1, (when - datetime.now(timezone.utc)).total_seconds())
        except (TypeError, ValueError, OverflowError):
            return fallback

    # A 429 rejects creation. Other ambiguous failures
    # are raised to the caller; never automatically repeat a POST after a timeout.
    for attempt in range(5):
        r = requests.post(f"{API}/v1/request", json=body, headers=HEADERS,
                          timeout=min(30, remaining()))
        if r.status_code != 429 or attempt == 4:
            break
        wait(retry_after(r, min(30, 2 ** attempt)))
    r.raise_for_status()
    solve_id = r.json()["id"]

    read_delay = 1
    while True:
        request_timeout = min(30, remaining())
        try:
            r = requests.get(f"{API}/v2/response/{solve_id}", headers=HEADERS,
                             timeout=request_timeout)
        except (requests.Timeout, requests.ConnectionError):
            wait(read_delay)
            read_delay = min(30, read_delay * 2)
            continue  # Retry only this result read; never create another solve.
        remaining()
        if r.status_code == 429 or 500 <= r.status_code < 600:
            wait(retry_after(r, read_delay))
            read_delay = min(30, read_delay * 2)
            continue  # Reads are safe to retry for the same solve ID.
        r.raise_for_status()
        res = r.json()
        if res["ready"]:
            return solve_id, res
        read_delay = 1
        wait(1)  # HTTP 202 + ready:false: the solve is still running.


body = {
    "kind": "geetest",
    "url": "https://your-domain.example/login",
    "siteKey": "YOUR_GEETEST_CAPTCHA_ID",
}

solve_id, res = solve(body)

if not res["success"]:
    raise RuntimeError(res.get("message", "solve failed"))  # not billed; create a new solve to retry

payload = res["geetest"]  # submit to the site's GeeTest validation endpoint
```

## Result

A finished solve returns HTTP 200 with `ready: true`:

```json
{
  "ready": true,
  "status": "SUCCESS",
  "success": true,
  "userAgent": "RETURNED_USER_AGENT",
  "secUa": "RETURNED_SEC_CH_UA",
  "token": "",
  "geetest": {
    "lot_number": "0241c2ef9dc0f0e5a3d0e1f5f2ad5c8d",
    "captcha_output": "5KAmMKVkldquKZ8DP5ehcG8LDv1brhRYgCbj9...",
    "pass_token": "8e035d781cd7f7c1d6c2f45ae2b0b8c8fb...",
    "gen_time": "1735689600"
  }
}
```

| Field | Type | Description |
| --- | --- | --- |
| `geetest.lot_number` | string | v4 lot number |
| `geetest.captcha_output` | string | v4 captcha output |
| `geetest.pass_token` | string | v4 pass token |
| `geetest.gen_time` | string | v4 generation time |
| `geetest.geetest_challenge` | string | v3 challenge |
| `geetest.geetest_validate` | string | v3 validate value |
| `geetest.geetest_seccode` | string | v3 seccode |

Every v2 result carries `ready`, `status`, and `success`. Successful solves also provide `userAgent` and `secUa` for result usage; these may be empty while pending or on failure. Failed solves can include a `message`.

### Using the result

v4: the site owner verifies `lot_number`, `captcha_output`, `pass_token`, and `gen_time` through GeeTest’s validate endpoint, using the HMAC-SHA256 `sign_token`.

v3: submit `geetest_challenge`, `geetest_validate`, and `geetest_seccode` to the site’s validate endpoint. The result is single-use, so use it right away.

When using a successful result, preserve the returned `userAgent` as `User-Agent` and `secUa` as `Sec-CH-UA`.

If the target rejects a `SUCCESS` result, report it with `POST /v1/report/{id}`, using the saved `solve_id`:

```python
report = requests.post(f"{API}/v1/report/{solve_id}", headers=HEADERS, timeout=30)
report.raise_for_status()
```

Reports are accepted for 10 minutes after the solve finished; after that the report endpoint returns HTTP 404 `Request Not Found`.

## Protocol recap (same for every product)

- Base URL `https://api.captcha.fun`; auth header `API_KEY`; read the key from `CAPTCHAFUN_API_KEY`.
- `POST /v1/request` → `{"id": "…", "message": "Started Solving"}`. One call is one solve; do not re-send it to check progress.
- Poll `GET /v2/response/{id}` about once a second: HTTP 202 + `ready: false` while solving, HTTP 200 + `ready: true` when done. Do not use `GET /v1/response/{id}` unless you want a 60 s long-poll that reports `FAILURE` on timeout even if the solve is still running.
- A failed solve is HTTP 200 with `status: "FAILURE"` and a `message`; it is not billed. Only successful solves are charged.
- Always send the returned `userAgent` with the result. Cookie and header results are also bound to the solving session's egress IP: use the `_proxy` kind with a sticky `proxy` if your requests will come from elsewhere. `platform` is `windows` (default) or `mac`.
- HTTP errors are `{"message": "…"}`: 400 = fix the request (`API_KEY Header Missing` if no key is sent; `API_KEY Incorrect` if the solve belongs to another key), 429 = wait `Retry-After`. Retry 500 errors on result reads with backoff; do not automatically repeat a creation request whose outcome is unknown.

Full protocol: [overview](https://www.captcha.fun/docs/md/overview.md). Machine-readable: [OpenAPI JSON](https://www.captcha.fun/docs/openapi.json) (schema `GeetestSolveRequest`). [API reference](https://www.captcha.fun/docs/api/geetest).
