Every endpoint lives under https://fuckcaptcha.top and authenticates with Authorization: Basic <your key>. Jobs are asynchronous: submit to get a job id, then poll for the result.
Where to send requests. If you already call another provider, change the domain and nothing else — same request bodies, same responses.
Almost every endpoint is submit-then-poll, because recognition is not instant.
{"data":"nsj_…"}. You are charged the moment the upstream accepts it{"error":14}; when it finishes you get {"data":…}The billing rule, plainly: a rejected submit (bad parameters, not enough credit, no upstream capacity) costs nothing; once the upstream accepts the credits are deducted and never move again — whatever the outcome, including a timeout. No refunds. The upstream bills us per attempt and we pass that through at cost, with no markup.
The two entry points solve different problems; picking wrong just burns credit:
| Your situation | Use | Because |
|---|---|---|
| You already have a captcha image (screenshot, scraped image) | /v1/recognition | Tells you which tiles to click / what the image says; you do the clicking |
| You want a submittable token and do not care how | /v1/token | The whole challenge is solved server-side and you get the token |
| You use the browser extension | Nothing to integrate | The extension calls /v1/recognition itself; you just paste your key |
Either form works:
Authorization: Basic nsk_YOUR_KEY
Note: not base64. Put the key after Basic verbatim, do not encode it.
?key=nsk_YOUR_KEY
Handy for trying things in a browser. This is also what our extension uses.
Keys are created in the dashboard and can be revoked at any time. If a key leaks, revoke it and issue a new one — the old one dies immediately.
Using the cheapest type, text captchas, the whole flow is three steps.
# the captcha image, base64-encoded, goes in image_data curl -X POST {base}/v1/recognition/textcaptcha \ -H 'Authorization: Basic nsk_YOUR_KEY' \ -H 'Content-Type: application/json' \ -d '{"image_data":["data:image/png;base64,iVBORw0KGgo…"]}' # → {"data":"nsj_f350ba75d856c66b0b924340"}
# while it is still working you get error 14 — just ask again in 500ms curl {base}/v1/recognition/textcaptcha?id=nsj_f350ba75d856c66b0b924340 \ -H 'Authorization: Basic nsk_YOUR_KEY' # pending → {"error":14,"message":"Incomplete job"} # finished → {"data":["H7K5"],"metadata":{}}
Type the recognised text into the target site's field and submit the form.
A ready-made function with polling and a timeout. Copy it.
import base64, time, requests BASE = "{base}" KEY = "nsk_YOUR_KEY" AUTH = {"Authorization": f"Basic {KEY}"} def solve(image_path, kind="textcaptcha", timeout=180): # 1) submit with open(image_path, "rb") as f: b64 = base64.b64encode(f.read()).decode() r = requests.post( f"{BASE}/v1/recognition/{kind}", headers={**AUTH, "Content-Type": "application/json"}, json={"image_data": [b64]}, ) job = r.json() if "error" in job: raise RuntimeError(job) # rejected on submit — nothing charged job_id = job["data"] # 2) poll (start at 500ms, back off) deadline, wait = time.time() + timeout, 0.5 while time.time() < deadline: time.sleep(wait) wait = min(wait * 1.2, 3) out = requests.get( f"{BASE}/v1/recognition/{kind}", headers=AUTH, params={"id": job_id}, ).json() if out.get("error") == 14: # still working continue if "error" in out: # accepted then failed: charged, no refund raise RuntimeError(out) return out["data"] raise TimeoutError(job_id) print(solve("captcha.png")) # NB: Python's stdlib urllib is blocked by the CDN as a bot (403 error 1010). # Use requests (above), or give urllib a normal User-Agent.
Node 18+ ships fetch, so there is nothing to install.
const BASE = "{base}" const KEY = "nsk_YOUR_KEY" const AUTH = { Authorization: `Basic ${KEY}` } async function solve(imageBase64, kind = "textcaptcha", timeoutMs = 180000) { // 1) submit const post = await fetch(`${BASE}/v1/recognition/${kind}`, { method: "POST", headers: { ...AUTH, "Content-Type": "application/json" }, body: JSON.stringify({ image_data: [imageBase64] }), }).then((r) => r.json()) if (post.error) throw new Error(JSON.stringify(post)) // rejected on submit — nothing charged // 2) poll const until = Date.now() + timeoutMs let wait = 500 while (Date.now() < until) { await new Promise((r) => setTimeout(r, wait)) wait = Math.min(wait * 1.2, 3000) const out = await fetch( `${BASE}/v1/recognition/${kind}?id=${post.data}`, { headers: AUTH }, ).then((r) => r.json()) if (out.error === 14) continue // still working if (out.error) throw new Error(JSON.stringify(out)) // accepted then failed: charged return out.data } throw new Error("timeout: " + post.data) }
| Method | Path | Description |
|---|---|---|
| GET | /v1/status/ | Remaining credit and reset countdown |
| POST | /v1/recognition/<type> | Submit an image recognition job |
| GET | /v1/recognition/<type>?id= | Fetch the recognition result |
| POST | /v1/token/<type> | Submit a token job |
| GET | /v1/token/<type>?id= | Fetch the token |
| POST | / | Root recognition endpoint, type in the body |
| GET | /?id= | Fetch the result of a root job |
| GET | /healthz | Health check, no key needed |
| POST | /v1/observability | Used by the extension to report telemetry; returns {} — you can ignore it |
Compatibility: the legacy paths /token/, /recognition/ and /status/ (without /v1) work too, and the type may be omitted from the path and sent in the body instead.
curl {base}/v1/status/ -H 'Authorization: Basic nsk_YOUR_KEY'
Response
{
"plan": "starter",
"status": "Active",
"credit": 914, // how many you can still use
"quota": 1000, // allowance of this plan
"balance": 0, // always 0 (kept for client compatibility)
"duration": 42689, // seconds until the next reset
"lastreset": 1789401600, // last reset, Unix seconds
"concurrency": 4 // parallel solves allowed
}
| Field | Description |
|---|---|
| plan | Plan name |
| status | Active is normal; anything else means the account is restricted |
| credit | What you can spend right now. Submitting with no credit left returns error 16. |
| quota | The allowance of this plan. |
| balance | Always 0. Kept only so the official client stays compatible; there is no top-up balance on this site. |
| duration | Seconds. At 0 it refills to full |
| lastreset | Unix timestamp of the last reset |
| concurrency | Parallel solve limit; exceeding it returns error 11 |
Hand us the captcha image you are looking at; we tell you which tiles to click, or what the image says.
| Field | Description |
|---|---|
| image_data | Required. Array of images — raw base64, or a data:image/png;base64,… URI |
| task | The challenge text, e.g. Select all images with traffic lights. Send it when you have it; accuracy improves |
| grid | Grid size, e.g. 3x3, 4x4. Optional |
| data | For hCaptcha. Pass the data object you scraped from the page through unchanged |
| type | The type may also go here, for when the path has none (POST /v1/recognition) |
Every response is wrapped as {data, metadata}. metadata is an empty object today, reserved for future use — pass it along as-is.
curl -X POST {base}/v1/recognition/textcaptcha \ -H 'Authorization: Basic nsk_YOUR_KEY' \ -H 'Content-Type: application/json' \ -d '{"image_data":["iVBORw0KGgoAAAANSUhEUg…"]}' # → {"data":"nsj_f350ba75d856c66b0b924340"}
{
"data": ["H7K5"],
"metadata": {}
}
The result is a boolean array: true means the tile at that position should be selected, in the same order as the image_data you sent.
curl -X POST {base}/v1/recognition/recaptcha \ -H 'Authorization: Basic nsk_YOUR_KEY' \ -H 'Content-Type: application/json' \ -d '{ "task": "Select all images with traffic lights", "grid": "3x3", "image_data": ["<tile 1>", "<tile 2>", "…9 in total"] }' # → {"data":[true,false,true,false,true,false,false,false,true]}
hCaptcha does not use image_data — you pass the whole data object from the page, which already carries the question and the images.
curl -X POST {base}/v1/recognition/hcaptcha \ -H 'Authorization: Basic nsk_YOUR_KEY' \ -H 'Content-Type: application/json' \ -d '{ "data": { "request_type": "image_label_binary", "requester_question": { "en": "Please click each image containing a cat" }, "tasklist": [ { "datapoint_uri": "data:image/png;base64,iVBORw0KGgo…", "task_key": "11111111-2222-3333-4444-555555555555" } ] } }'
For Turnstile, "recognition" only does the server side and returns a boolean ack ({"data":true}); the client clicks the checkbox itself. Just send the page URL.
curl -X POST {base}/v1/recognition/turnstile \ -H 'Authorization: Basic nsk_YOUR_KEY' \ -H 'Content-Type: application/json' \ -d '{"url":"https://example.com/login"}' # poll → {"data":true,"metadata":{}}
Coordinates come back as percentages of the image: top-left is {x:0,y:0}, bottom-right is {x:100,y:100}. Convert with data.x / 100 * image width.
{
"data": {
"x": 72.5,
"y": 34.1
}
}
The shape of the result (string array / boolean array / coordinates / boolean ack) depends on the type — read it off data. Those are the types the extension uses; the API passes the type name straight through, so any other type the upstream supports works too.
No image upload: the system solves the whole challenge in the background and hands back a token you can submit to the target site. Meant for reCAPTCHA, Turnstile and anything else whose result is a token.
This endpoint is slow — a measured hCaptcha takes about a minute — so token jobs get a 5-minute job id (recognition jobs only get 3). Poll every 2–3 seconds, not faster; every accepted job is charged, so resubmitting just pays twice.
| Field | Description |
|---|---|
| sitekey | Required. The target site's sitekey (data-sitekey) |
| url | Required. The URL of the page showing the captcha; it must match the real page |
| useragent | Recommended. The target browser's UA — it noticeably improves the pass rate |
| data | Extra parameters, e.g. hCaptcha's rqdata or reCAPTCHA v3's action |
| enterprise | Send true for reCAPTCHA Enterprise |
| proxy | Send this to pin an egress IP, shaped like {"scheme":"http","host":"…","port":8080,"username":"…","password":"…"} |
| type | The type may also go here when the path has none |
curl -X POST {base}/v1/token/hcaptcha \ -H 'Authorization: Basic nsk_YOUR_KEY' \ -H 'Content-Type: application/json' \ -d '{ "sitekey": "10000000-ffff-ffff-ffff-000000000001", "url": "https://accounts.hcaptcha.com/demo", "useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36" }' # → {"data":"nsj_1efe69a3296e64860b7ac5f6"} the job id
curl {base}/v1/token/hcaptcha?id=nsj_1efe69a3296e64860b7ac5f6 \ -H 'Authorization: Basic nsk_YOUR_KEY' # pending → {"error":14,"message":"Incomplete job"} # finished → {"data":"P0_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…"} this IS the token
Tokens expire (usually within 1–2 minutes). Use them immediately; do not cache them.
If your code already calls another provider's root endpoint, this one is byte-compatible: change the domain, keep everything else. Here type goes in the body, not the path.
curl -X POST {base}/ \ -H 'Authorization: Basic nsk_YOUR_KEY' \ -H 'Content-Type: application/json' \ -d '{ "type": "textcaptcha", "image_data": ["<base64>"] }' # → {"data":"nsj_7271aaaa0e796d23b42d29e2"} # poll curl {base}/?id=nsj_7271aaaa0e796d23b42d29e2 -H 'Authorization: Basic nsk_YOUR_KEY'
Forgetting type returns {"error":10,"message":"Missing `type`"}.
Supported types and what each one costs. Image recognition is billed per image, tokens per mint.
/v1/recognition/<type>| type | Captcha | Cost per call |
|---|---|---|
| textcaptcha | Text captcha (distorted letters and digits) | 1 |
| hcaptcha | hCaptcha tile selection / drag | 1 |
| recaptcha | reCAPTCHA image selection | 1 |
| funcaptcha | FunCaptcha / Arkose puzzle | 1 |
| funcaptcha_match | FunCAPTCHA image match (returns a boolean array) | 1 |
| turnstile | Cloudflare Turnstile (returns a boolean ack) | 1 |
| awscaptcha | AWS WAF audio / image | 1 |
| geetest | GeeTest slider (returns data.x) | 1 |
| perimeterx | Human (PerimeterX) | 1 |
| lemincaptcha | Lemin puzzle | 1 |
/v1/token/<type>| type | Captcha | Cost per call |
|---|---|---|
| hcaptcha | hCaptcha token | 10 |
| recaptcha2 | reCAPTCHA v2 token | 20 |
| recaptcha3 | reCAPTCHA v3 token | 20 |
| turnstile | Cloudflare Turnstile token | 1 |
Submitting without enough credit returns error 16 straight away — it never charges first and fails after.
Errors come back as {"error":<code>,"message":"…"}. Nearly every error has HTTP status 200, so branch on error, never on the status code. Only three cases differ:
| HTTP | When | Body |
|---|---|---|
| 401 | Key missing, wrong or revoked | {"error":15,…} |
| 405 | Wrong method (e.g. GET to submit a job) | {"error":10,…} |
| 404 | Wrong path | {"error":10,…} |
Our gateway only ever returns these error values:
| error | Meaning | What to do |
|---|---|---|
| 9 | Unknown error / no capacity / an accepted job failed or timed out | Rejected at submit: nothing charged, retry is fine. Accepted then failed: already charged, no refund |
| 10 | Invalid request (missing field, wrong method or path) | Check your parameters against this page; retrying will not help |
| 11 | Rate or concurrency limit exceeded | Slow down, reduce concurrency, then retry |
| 13 | Job id not found | The id is wrong, or the job belongs to a different key |
| 14 | Job not finished yet | Poll again in 500ms. This is not a failure and costs nothing extra |
| 15 | Invalid API key | Check the key, or issue a new one in the dashboard |
| 16 | Not enough credit | Buy a plan, or wait for the next daily reset |
The upstream has a few error codes of its own (12 banned IP, 17 client must update, 18 type unavailable); we fold all of them into 9 with the original message attached, so clients never handle them separately. A mistyped type name (e.g. hcapcha instead of hcaptcha) also reaches the upstream and comes back as 9 — check the spelling first when you see it.
error 11. Higher limits are available on requesterror 11. Queue bulk work instead of pushing throughAuthorization: Basic nsk_YOUR_KEY — the raw key goes right after Basic. That is deliberate, for compatibility with existing clients; encoding it again makes it unrecognisable.data:image/png;base64,… URI. Screenshot it and base64 it — no compression needed, but do not crop or distort it: tile-selection challenges depend on the relative positions of the images.{x:0,y:0}, bottom-right is {x:100,y:100}. Multiply by the image's real width and height.error 11. For bulk work, run a queue and cap the concurrency instead of firing everything at once.task (the challenge text) — accuracy improves noticeably. Pass hCaptcha's data object through unchanged too; it carries the link between the question and the images. For token types, keep useragent consistent with the target page.