# DataDome: Captcha.fun API

> Solve DataDome challenges and replay the `datadome` cookie.

## At a glance

- **`kind`**: `datadome` (Captcha.fun's IPs), or `datadome_proxy` with a `proxy` (your own IP)
- **You send**: `url` (optional: `challengeHtml`)
- **You get**: datadome cookie → `token`, `cookies`, `cookieHeader`
- **Default price**: $2.00 per 1,000 successful solves (20 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

- Returns the `datadome` cookie value, `cookieHeader`, and `userAgent` for requests to the protected site.
- By default the solve targets your URL directly. If your flow uses the invisible-tags variant, pass `challengeHtml` with the captured protected-page HTML.
- **Important:** The cookie is bound to the session and IP. If your requests won’t come from the solve’s IP, use a `_proxy` kind with a sticky proxy.

## Request: `POST /v1/request`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | string | yes | `datadome` · `datadome_proxy` |
| `url` | string | yes | Page URL where the challenge appears. |
| `challengeHtml` | string | no | Captured page HTML for the invisible-tags flow. Omit to use the live slider on `url` |
| `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": "datadome_proxy",
  "url": "https://your-domain.example/protected-page",
  "proxy": "proxy.example:8080:USERNAME:PASSWORD"
}
```

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

Set `CAPTCHAFUN_PROXY` to the same sticky HTTP proxy used for the solve and replay: `host:port` or `host:port:username:password`. Keep its egress IP stable.

```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": "datadome_proxy",
    "url": "https://your-domain.example/protected-page",
    "proxy": "proxy.example:8080:USERNAME:PASSWORD",
}
from urllib.parse import quote

proxy = os.environ["CAPTCHAFUN_PROXY"]
parts = proxy.split(":")
if len(parts) not in (2, 4):
    raise ValueError("CAPTCHAFUN_PROXY must be host:port or host:port:username:password")
credentials = f"{quote(parts[2], safe='')}:{quote(parts[3], safe='')}@" if len(parts) == 4 else ""
proxy_url = f"http://{credentials}{parts[0]}:{parts[1]}"
proxies = {"http": proxy_url, "https": proxy_url}
body["proxy"] = proxy

solve_id, res = solve(body)

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

# Replay through the same sticky proxy, preserving the returned client headers.
headers = {"User-Agent": res["userAgent"], "Sec-CH-UA": res["secUa"], "Cookie": res["cookieHeader"]}
target_url = body["url"]

def replay(method, request_body=None, *, content_type=None):
    request_headers = dict(headers)
    if content_type is not None:
        request_headers["Content-Type"] = content_type
    response = requests.request(method, target_url, data=request_body,
                                headers=request_headers, proxies=proxies, timeout=30)
    if response.status_code >= 400:
        raise requests.HTTPError(f"Target returned HTTP {response.status_code} (solve ID: {solve_id}); inspect error.response and report this ID if the solve result was rejected", response=response)
    return response

# Choose your endpoint's method and body before calling replay.
# For JSON bytes, explicitly select application/json:
# response = replay("POST", request_body=b'{"replace": "YOUR_PAYLOAD"}', content_type="application/json")
# Inspect the response body too: an HTTP 200 can still indicate a rejected result.
```

## 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": "a1b2c3d4e5f6g7h8i9j0~0~...,",
  "cookies": {
    "datadome": "a1b2c3d4e5f6g7h8i9j0~0~...,"
  },
  "cookieHeader": "datadome=a1b2c3d4e5f6g7h8i9j0~0~...,"
}
```

| Field | Type | Description |
| --- | --- | --- |
| `token` | string | The trusted datadome cookie value minted by passing the challenge |
| `cookies` | object | The defense cookie family, cookie name to value |
| `cookieHeader` | string | The solving session's full Cookie header value, verbatim — replay it with the returned userAgent from the same egress IP |

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

Replay cookieHeader as the Cookie header on outbound requests to the protected origin (or token as the bare datadome cookie value), with the returned userAgent as your User-Agent header. The cookie is bound to the solving session, so keep the replay on the same egress IP.

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 `DatadomeSolveRequest`). [API reference](https://www.captcha.fun/docs/api/datadome).
