# Akamai (sec_cpt): Captcha.fun API

> Solve Akamai `sec_cpt` proof-of-work challenges and replay the cookies.

## At a glance

- **`kind`**: `akamai_sec_cpt` (Captcha.fun's IPs), or `akamai_sec_cpt_proxy` with a `proxy` (your own IP)
- **You send**: `url` (optional: `triggerUrl`, `triggerBodyJson`, `protectedUrl`)
- **You get**: sec_cpt cookies → `token`, `cookies`, `cookieHeader`
- **Default price**: $2.50 per 1,000 successful solves (25 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

- The Akamai sec_cpt product solves the 428 sec-cp-challenge proof-of-work in a real browser: the solver triggers the challenge from the page context, works the proof, and returns the solved sec_cpt cookie plus any Akamai session siblings.
- The trigger URL defaults per site; pass triggerUrl with an optional JSON body when your deployment's challenge fires on a specific API call. protectedUrl names the endpoint you will replay against (advisory — not a pre-return validation).

## Request: `POST /v1/request`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | string | yes | `akamai_sec_cpt` · `akamai_sec_cpt_proxy` |
| `url` | string | yes | Page URL where the challenge appears. |
| `triggerUrl` | string | no | API URL whose 428 response triggers the challenge. Has per-site defaults |
| `triggerBodyJson` | string | no | Raw JSON body for the trigger request — paired with triggerUrl |
| `protectedUrl` | string | no | Endpoint you will replay the session against. Defaults to `url`. Advisory — the solver does not validate against it before return |
| `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": "akamai_sec_cpt_proxy",
  "url": "https://your-domain.example",
  "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": "akamai_sec_cpt_proxy",
    "url": "https://your-domain.example",
    "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.get("protectedUrl") or 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": "2B3C4D5E6F7G8H9I0J1K~0~...,",
  "cookies": {
    "sec_cpt": "2B3C4D5E6F7G8H9I0J1K~0~...,",
    "bm_sz": "bm-sz-sibling..."
  },
  "cookieHeader": "sec_cpt=2B3C4D5E6F7G8H9I0J1K~0~...,; bm_sz=bm-sz-sibling..."
}
```

| Field | Type | Description |
| --- | --- | --- |
| `token` | string | The solved sec_cpt cookie value |
| `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, with the returned userAgent as your User-Agent header, from the same egress IP as the solve.

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