# ThreatMetrix: Captcha.fun API

> Bind a ThreatMetrix device profile to your session ID.

## At a glance

- **`kind`**: `threatmetrix` (Captcha.fun's IPs), or `threatmetrix_proxy` with a `proxy` (your own IP)
- **You send**: `url`, `threatMetrixOrgId`, `threatMetrixSessionId`
- **You get**: Your session ID, bound to a profile → `token`
- **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

- Runs ThreatMetrix device profiling and binds the profile to the session ID you supply. No domain input is needed.
- There’s no token to replay: after the solve, send the same session ID with your protected call.

## Request: `POST /v1/request`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | string | yes | `threatmetrix` · `threatmetrix_proxy` |
| `url` | string | yes | Page URL where the challenge appears. |
| `threatMetrixOrgId` | string | yes | Your ThreatMetrix org ID: the account the profile binds to |
| `threatMetrixSessionId` | string | yes | Session ID to bind the profile to. Send the same ID with the protected call |
| `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": "threatmetrix",
  "url": "https://your-domain.example",
  "threatMetrixOrgId": "YOUR_THREATMETRIX_ORG_ID",
  "threatMetrixSessionId": "YOUR_TRANSACTION_SESSION_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": "threatmetrix",
    "url": "https://your-domain.example",
    "threatMetrixOrgId": "YOUR_THREATMETRIX_ORG_ID",
    "threatMetrixSessionId": "YOUR_TRANSACTION_SESSION_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

token = res["token"]  # Your session ID, bound to a profile
```

## 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": "YOUR_TRANSACTION_SESSION_ID"
}
```

| Field | Type | Description |
| --- | --- | --- |
| `token` | string | Your session ID, echoed once the profile is bound |

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

Send the same session ID with the protected login or payment call. ThreatMetrix scores it against the profile bound during the solve.

The profile is bound to the solving browser's fingerprint and egress IP — send the protected call from the same egress. Profiles accumulate trust server-side; a session id solved seconds ago starts with minimal history.

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