# Captcha.fun API

> One API key, one balance, every captcha and anti-bot solver.

Use this page for the shared contract; each product has its own linked brief with exact fields and result handling. The machine-readable version is [OpenAPI JSON](https://www.captcha.fun/docs/openapi.json). Documentation describes inputs and outputs; product availability depends on the catalog and your API key.

## Quick start

Every product works the same way:

1. `POST /v1/request` with the product `kind` and the page details. You get a solve `id` immediately.
2. `GET /v2/response/{id}` about once a second until `ready` is `true`.
3. Use the result (a token to submit, cookies to replay, or headers to replay), always with the returned `userAgent`.

Install `requests` (`python -m pip install requests`) and set `CAPTCHAFUN_API_KEY` in your environment. Replace the example URL and site key.

`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. The returned `solve_id` is retained for reporting a rejected result.

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


solve_id, res = solve({
    "kind": "turnstile",
    "url": "https://your-domain.example/login",
    "siteKey": "YOUR_TURNSTILE_SITE_KEY",
})
if not res["success"]:
    raise RuntimeError(res.get("message", "solve failed"))
print(res["token"])
```

Same thing with curl:

```bash
curl -X POST 'https://api.captcha.fun/v1/request' -H "API_KEY: $CAPTCHAFUN_API_KEY" -H 'Content-Type: application/json' \
  -d '{"kind":"turnstile","url":"https://your-domain.example/login","siteKey":"YOUR_TURNSTILE_SITE_KEY"}'
# {"id":"550e8400-e29b-41d4-a716-446655440000","message":"Started Solving"}

curl 'https://api.captcha.fun/v2/response/550e8400-e29b-41d4-a716-446655440000' -H "API_KEY: $CAPTCHAFUN_API_KEY"   # repeat until "ready": true
```

## Pick a product

The `kind` goes in the request body. Every product also has `<kind>_proxy`: it solves through your own `proxy` so the result comes from your IP. **You must send** lists the required body fields besides `kind`; if you do not have one, you cannot use that product yet. Price is USD per 1,000 successful solves at the default rate card.

### Captchas

A widget on a form. You get a token (or, for GeeTest, a payload) to submit with the form or API call.

| Product | `kind` | You must send | You get back | Price | Brief |
| --- | --- | --- | --- | --- | --- |
| reCAPTCHA v3 | `recap_v3`, `recap_v3_enterprise` | `url`, `siteKey`, `action` | reCAPTCHA v3 token (`token`) | `recap_v3` $1.00; `recap_v3_enterprise` $2.50 | [reCAPTCHA v3](https://www.captcha.fun/docs/md/recaptcha-v3.md) |
| reCAPTCHA v2 | `recap_v2`, `recap_v2_invis`, `recap_v2_enterprise` | `url`, `siteKey` | g-recaptcha-response token (`token`) | `recap_v2` $1.50; `recap_v2_invis` $1.50; `recap_v2_enterprise` $2.50 | [reCAPTCHA v2](https://www.captcha.fun/docs/md/recaptcha-v2.md) |
| Cloudflare Turnstile | `turnstile` | `url`, `siteKey` | cf-turnstile-response token (`token`) | $1.50 | [Cloudflare Turnstile](https://www.captcha.fun/docs/md/turnstile.md) |
| hCaptcha Enterprise | `hcaptcha` | `url`, `siteKey` | h-captcha-response token (`token`) | $1.50 | [hCaptcha Enterprise](https://www.captcha.fun/docs/md/hcaptcha.md) |
| hCaptcha Invisible | `hcaptcha_invis` | `url`, `siteKey` | h-captcha-response token (`token`) | $1.50 | [hCaptcha Invisible](https://www.captcha.fun/docs/md/hcaptcha-invisible.md) |
| GeeTest | `geetest` | `url`, `siteKey` | GeeTest v4 or v3 validate values (`geetest`) | $1.50 | [GeeTest](https://www.captcha.fun/docs/md/geetest.md) |
| Friendly Captcha | `friendly_captcha` | `url`, `siteKey` | frc-captcha-solution value (`token`) | $1.00 | [Friendly Captcha](https://www.captcha.fun/docs/md/friendly-captcha.md) |
| CaptchaFox | `captchafox` | `url`, `siteKey` | CaptchaFox token (`token`) | $1.50 | [CaptchaFox](https://www.captcha.fun/docs/md/captchafox.md) |
| FunCaptcha Invisible | `funcaptcha_invis` | `url`, `siteKey` | FunCaptcha-Token session token (`token`) | $1.50 | [FunCaptcha Invisible](https://www.captcha.fun/docs/md/funcaptcha-invisible.md) |

### Anti-bot

A block page, challenge, or sensor protecting a site. You get cookies or headers to replay on requests to the protected origin (AWS WAF returns a token you send as a cookie).

| Product | `kind` | You must send | You get back | Price | Brief |
| --- | --- | --- | --- | --- | --- |
| Cloudflare WAF | `cf_waf` | `url` | cf_clearance cookie (`cookie`) | $1.50 | [Cloudflare WAF](https://www.captcha.fun/docs/md/cloudflare-waf.md) |
| DataDome | `datadome` | `url` | datadome cookie (`cookie`) | $2.00 | [DataDome](https://www.captcha.fun/docs/md/datadome.md) |
| DataDome Interstitial | `datadome_interstitial` | `url` | datadome cookie (`cookie`) | $2.00 | [DataDome Interstitial](https://www.captcha.fun/docs/md/datadome-interstitial.md) |
| PerimeterX | `perimeterx` | `url`, `challengeHtml` | _px* clearance cookie (`cookie`) | $2.00 | [PerimeterX](https://www.captcha.fun/docs/md/perimeterx.md) |
| PerimeterX Hold | `perimeterx_hold` | `url`, `blockData` | _px3 and _pxhd cookies (`cookie`) | $3.00 | [PerimeterX Hold](https://www.captcha.fun/docs/md/perimeterx-hold.md) |
| PerimeterX Invisible | `perimeterx_invisible` | `url` | Returned _px cookie family (such as _px3 or _px2); additional cookies vary by target (`cookie`) | $2.00 | [PerimeterX Invisible](https://www.captcha.fun/docs/md/perimeterx-invisible.md) |
| Akamai (_abck) | `akamai_abck` | `url` | _abck session cookies (`cookie`) | $2.50 | [Akamai (_abck)](https://www.captcha.fun/docs/md/akamai-abck.md) |
| Akamai (sbsd/bm_sc) | `akamai_sbsd` | `url` | sbsd and bm_sc cookies (`cookie`) | $2.50 | [Akamai (sbsd/bm_sc)](https://www.captcha.fun/docs/md/akamai-sbsd.md) |
| Akamai (sec_cpt) | `akamai_sec_cpt` | `url` | sec_cpt cookies (`cookie`) | $2.50 | [Akamai (sec_cpt)](https://www.captcha.fun/docs/md/akamai-sec-cpt.md) |
| Kasada | `kasada` | `url`, `protectedUrl` | x-kpsdk-* headers (`headers`) | $5.00 | [Kasada](https://www.captcha.fun/docs/md/kasada.md) |
| Kasada (cd) | `kasada_cd` | `url`, `protectedUrl` | x-kpsdk-cd headers (`headers`) | $5.00 | [Kasada (cd)](https://www.captcha.fun/docs/md/kasada-cd.md) |
| Kasada (tl payload) | `kasada_tl_payload` | `url` | A replayable /tl request (`headers`) | $5.00 | [Kasada (tl payload)](https://www.captcha.fun/docs/md/kasada-tl-payload.md) |
| Imperva | `imperva` | `url` | Clearance cookies (reese84 plus session cookies) (`cookie`) | $2.00 | [Imperva](https://www.captcha.fun/docs/md/imperva.md) |
| Incapsula (_utmvc) | `incapsula_utmvc` | `url` | _utmvc cookies (`cookie`) | $1.50 | [Incapsula (_utmvc)](https://www.captcha.fun/docs/md/incapsula-utmvc.md) |
| Incapsula (reese84) | `reese84` | `url` | reese84 cookie (`cookie`) | $1.50 | [Incapsula (reese84)](https://www.captcha.fun/docs/md/reese84.md) |
| AWS WAF | `aws_waf` | `url` | aws-waf-token cookie (`token`) | $2.00 | [AWS WAF](https://www.captcha.fun/docs/md/aws-waf.md) |
| Shape | `shape` | `url`, `challengeHtml`, `protectedUrl` | Shape request headers (`headers`) | $5.00 | [Shape](https://www.captcha.fun/docs/md/shape.md) |
| Vercel Challenge | `vercel_challenge` | `url` | Clearance cookies (`cookie`) | $1.50 | [Vercel Challenge](https://www.captcha.fun/docs/md/vercel-challenge.md) |

### Risk and session

Device-risk and session products. You get a token, or cookies, to send with the protected call.

| Product | `kind` | You must send | You get back | Price | Brief |
| --- | --- | --- | --- | --- | --- |
| Ticketmaster TMPT | `tmpt` | `url` | tmpt session token (`cookie`) | $2.50 | [Ticketmaster TMPT](https://www.captcha.fun/docs/md/tmpt.md) |
| Castle | `castle` | `url` | One-time X-Castle-Request-Token (`token`) | $2.00 | [Castle](https://www.captcha.fun/docs/md/castle.md) |
| Forter | `forter` | `url` | Forter token for the next request (`token`) | $2.00 | [Forter](https://www.captcha.fun/docs/md/forter.md) |
| Riskified | `riskified` | `url` | Riskified cookies (`cookie`) | $2.00 | [Riskified](https://www.captcha.fun/docs/md/riskified.md) |
| ThreatMetrix | `threatmetrix` | `url`, `threatMetrixOrgId`, `threatMetrixSessionId` | Your session ID, bound to a profile (`token`) | $1.50 | [ThreatMetrix](https://www.captcha.fun/docs/md/threatmetrix.md) |

Prices come from the default rate card and may differ for your key. `GET /v1/products` returns `defaultCreditCost` per product (credits per solve ÷ 10 = USD per 1,000 solves).

Result types: **token** = one value to submit; **cookie** = send `cookieHeader` as the `Cookie` header; **headers** = replay `headers` on one request to exactly the `protectedUrl` you gave; **geetest** / **text** = a payload or string to use directly.

## Protocol reference

### Authentication

Send `API_KEY: <key>` on `/v1/request`, `/v1/response/{id}`, `/v2/response/{id}`, `/v1/report/{id}` and `/v1/stats`. Keys look like `CAPFUN-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`. Read the key from `CAPTCHAFUN_API_KEY`; never hardcode or log it. A missing or wrong key is HTTP 400, not 401. Optional `APP_ID` header tags a request for tracking.

### Endpoints

| Method | Path | Purpose |
| --- | --- | --- |
| POST | `/v1/request` | Create a solve for any product. Body: JSON with `kind` plus the product's fields. Returns `{id, message}`. |
| GET | `/v2/response/{id}` | Result. HTTP 202 + `ready:false` while solving; HTTP 200 + `ready:true` when finished. Use this one. |
| GET | `/v1/response/{id}` | Long-poll: holds the connection up to 60 s, then returns `status:FAILURE` on timeout even if the solve is still running. |
| POST | `/v1/report/{id}` | Report a `SUCCESS` result that the target rejected, using the saved solve id. Accepted for 10 minutes after the solve finished. Reporting does not change the result returned by subsequent reads. |
| GET | `/v1/stats` | `{credits, success, failures}` for the key. |
| GET | `/v1/products` | Products and `defaultCreditCost`. No auth. |

### Result envelope

Both result endpoints return these fields for every product, plus the product-specific ones in its brief.

| Field | Type | Description |
| --- | --- | --- |
| ready | boolean | v2 only. `true` once the solve has finished, successfully or not. |
| status | string | `PENDING`, `SUCCESS`, or `FAILURE`. Reporting a rejected result does not change the result-read status. |
| success | boolean | `true` only when `status` is `SUCCESS`. |
| userAgent | string | On successful solves, send this value as `User-Agent`. Preserve it exactly; it can be empty while pending or on failure. |
| secUa | string | On successful solves, send this value as `Sec-CH-UA`; it can be empty while pending or on failure. |
| message | string | Failure reason; present only when the solve failed. |

Product-specific fields: `token` (captcha tokens, AWS WAF, Castle, Forter, ThreatMetrix), `cookies` + `cookieHeader` (cookie products; `token` is the main cookie's bare value), `headers` (Shape, Kasada), `body` + `url` (Kasada tl payload), `geetest`.

```json
// pending: HTTP 202
{
  "status": "PENDING",
  "success": false,
  "userAgent": "",
  "secUa": "",
  "token": "",
  "ready": false
}

// failed: HTTP 200
{
  "ready": true,
  "status": "FAILURE",
  "success": false,
  "userAgent": "",
  "secUa": "",
  "token": "",
  "message": "<failure reason>"
}
```

### Billing

Only successful solves are charged. A solve that ends in `FAILURE` is not billed; create a new solve to retry. Check balance with `GET /v1/stats`.

### Proxy and platform

- Non-proxy kinds use Captcha.fun's IPs. `<kind>_proxy` kinds solve through your `proxy` (`host:port` or `host:port:username:password`); if you send a `_proxy` kind without a proxy, Captcha.fun's IPs are used.
- When replaying cookies or headers, preserve the returned `userAgent` and use the same egress IP as the solve. Use the `_proxy` kind with a **sticky** proxy when you need to control that IP.
- `platform` (all products): `windows` (default) or `mac`.

### Replaying results

- Send the returned `userAgent` (and `secUa`) with the result. Never substitute your own.
- Tokens are single-use and short-lived: submit immediately. Header sets are single-use; Shape and Kasada sets are bound to the endpoint you passed as `protectedUrl`, so solve again for each endpoint.
- Prefer the full `cookieHeader` over rebuilding a cookie jar from `cookies`.

### Errors

| HTTP | When |
| --- | --- |
| 400 | Bad request. Messages: `API_KEY Header Missing`, `API_KEY Does Not Exist`, `API_KEY Has No Credits`, `API_KEY does not support this product`, `invalid kind`, `<kind> solve requires <field>`, `Invalid proxy format`; on result endpoints `API_KEY Incorrect` means the id belongs to another key. Fix the request; do not retry unchanged. |
| 429 | Per-key request limit hit. Honor `Retry-After` (seconds or an HTTP-date), then retry. |
| 500 | Server error. Retry result reads with backoff. If creation times out or returns a server error, its outcome can be unknown: do not automatically create another solve, which could be billed again. |

[Docs](https://www.captcha.fun/docs) · [OpenAPI JSON](https://www.captcha.fun/docs/openapi.json)
