API Reference

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.

Overview

Where to send requests. If you already call another provider, change the domain and nothing else — same request bodies, same responses.

BASE

The async job model

Almost every endpoint is submit-then-poll, because recognition is not instant.

POSTSubmit a jobReturns a job id {"data":"nsj_…"}. You are charged the moment the upstream accepts it
GET?id=nsj_…While it is working you get {"error":14}; when it finishes you get {"data":…}
TTL3 / 5 minutesRecognition jobs live 3 minutes, token jobs 5 minutes (tokens are simply slower). An expired job is void and must be resubmitted

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.

Which endpoint to use

The two entry points solve different problems; picking wrong just burns credit:

Your situationUseBecause
You already have a captcha image (screenshot, scraped image)/v1/recognitionTells you which tiles to click / what the image says; you do the clicking
You want a submittable token and do not care how/v1/tokenThe whole challenge is solved server-side and you get the token
You use the browser extensionNothing to integrateThe extension calls /v1/recognition itself; you just paste your key

Authentication

Either form works:

Request header (recommended)
Authorization: Basic nsk_YOUR_KEY

Note: not base64. Put the key after Basic verbatim, do not encode it.

Query parameter
?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.

Quickstart

Using the cheapest type, text captchas, the whole flow is three steps.

1. Submit a job

# 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"}

2. Poll for the result

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

3. Use it

Type the recognised text into the target site's field and submit the form.

Full example (Python)

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.

Full example (Node.js)

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)
}

Endpoint list

MethodPathDescription
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/healthzHealth check, no key needed
POST/v1/observabilityUsed 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.

Check balance

GET/v1/status/Remaining credit and reset countdown
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
}
FieldDescription
planPlan name
statusActive is normal; anything else means the account is restricted
creditWhat you can spend right now. Submitting with no credit left returns error 16.
quotaThe allowance of this plan.
balanceAlways 0. Kept only so the official client stays compatible; there is no top-up balance on this site.
durationSeconds. At 0 it refills to full
lastresetUnix timestamp of the last reset
concurrencyParallel solve limit; exceeding it returns error 11

Image recognition

Hand us the captcha image you are looking at; we tell you which tiles to click, or what the image says.

POST/v1/recognition/<type>Submit a job, get a job id
GET/v1/recognition/<type>?id=…Fetch the result

Request body

FieldDescription
image_dataRequired. Array of images — raw base64, or a data:image/png;base64,… URI
taskThe challenge text, e.g. Select all images with traffic lights. Send it when you have it; accuracy improves
gridGrid size, e.g. 3x3, 4x4. Optional
dataFor hCaptcha. Pass the data object you scraped from the page through unchanged
typeThe type may also go here, for when the path has none (POST /v1/recognition)

Response

Every response is wrapped as {data, metadata}. metadata is an empty object today, reserved for future use — pass it along as-is.

Example: text captcha

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": {}
}

Example: tile selection (hCaptcha / reCAPTCHA)

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]}

Example: the hCaptcha data object

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" }
      ]
    }
  }'

Example: Turnstile

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":{}}

Example: drag and drop (FunCaptcha / hCaptcha drag)

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.

Token generation

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.

POST/v1/token/<type>Submit, get a job id
GET/v1/token/<type>?id=…Fetch the token

Request body

FieldDescription
sitekeyRequired. The target site's sitekey (data-sitekey)
urlRequired. The URL of the page showing the captcha; it must match the real page
useragentRecommended. The target browser's UA — it noticeably improves the pass rate
dataExtra parameters, e.g. hCaptcha's rqdata or reCAPTCHA v3's action
enterpriseSend true for reCAPTCHA Enterprise
proxySend this to pin an egress IP, shaped like {"scheme":"http","host":"…","port":8080,"username":"…","password":"…"}
typeThe type may also go here when the path has none

Example

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.

Root recognition endpoint

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

Types and pricing

Supported types and what each one costs. Image recognition is billed per image, tokens per mint.

Image recognition · /v1/recognition/<type>

typeCaptchaCost per call
textcaptchaText captcha (distorted letters and digits)1
hcaptchahCaptcha tile selection / drag1
recaptchareCAPTCHA image selection1
funcaptchaFunCaptcha / Arkose puzzle1
funcaptcha_matchFunCAPTCHA image match (returns a boolean array)1
turnstileCloudflare Turnstile (returns a boolean ack)1
awscaptchaAWS WAF audio / image1
geetestGeeTest slider (returns data.x)1
perimeterxHuman (PerimeterX)1
lemincaptchaLemin puzzle1

Token generation · /v1/token/<type>

typeCaptchaCost per call
hcaptchahCaptcha token10
recaptcha2reCAPTCHA v2 token20
recaptcha3reCAPTCHA v3 token20
turnstileCloudflare Turnstile token1

Submitting without enough credit returns error 16 straight away — it never charges first and fails after.

Error codes

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:

HTTPWhenBody
401Key missing, wrong or revoked{"error":15,…}
405Wrong method (e.g. GET to submit a job){"error":10,…}
404Wrong path{"error":10,…}

Our gateway only ever returns these error values:

errorMeaningWhat to do
9Unknown error / no capacity / an accepted job failed or timed outRejected at submit: nothing charged, retry is fine. Accepted then failed: already charged, no refund
10Invalid request (missing field, wrong method or path)Check your parameters against this page; retrying will not help
11Rate or concurrency limit exceededSlow down, reduce concurrency, then retry
13Job id not foundThe id is wrong, or the job belongs to a different key
14Job not finished yetPoll again in 500ms. This is not a failure and costs nothing extra
15Invalid API keyCheck the key, or issue a new one in the dashboard
16Not enough creditBuy 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.

Quota and limits

QuotaDaily at 00:00Resets at midnight Beijing time (UTC+8) back to full. It does not accumulate: yesterday's leftovers do not roll over
ChargingOn acceptanceA rejected submit (bad parameters, no credit, no upstream capacity) costs nothing. The moment the upstream accepts, credits are deducted and never move again
RefundsNoneThe upstream bills us per attempt and we pass that through at cost with no markup, so accepted jobs are not refunded
Concurrency4 by defaultHow many jobs may run at once; exceeding it returns error 11. Higher limits are available on request
Rate20 per secondPer-key request rate ceiling; exceeding it also returns error 11. Queue bulk work instead of pushing through
Timeout3 / 5 minutesRecognition jobs expire after 3 minutes and token jobs after 5; resubmit. Credits already charged are not refunded

FAQ

The submit succeeded but I keep getting error 14
The job is still running. Recognition normally finishes in seconds; tokens take about a minute and longer at peak. Past the timeout (3 minutes for recognition, 5 for tokens) the job is void — just resubmit. Do not resubmit just because you saw a 14: you will have many jobs racing at once, which is slower, and every accepted job is charged.
Does the key need base64 encoding?
No. Write Authorization: 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.
How do I send the image?
Either way: a raw base64 string, or a 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.
Are the coordinates pixels?
No, percentages. Top-left is {x:0,y:0}, bottom-right is {x:100,y:100}. Multiply by the image's real width and height.
How many jobs can run at once?
4 by default; exceeding it returns error 11. For bulk work, run a queue and cap the concurrency instead of firing everything at once.
What if recognition is wrong?
For tile-selection types, always send 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.
Your captcha type is not listed?
Write to admin@fuckcaptcha.top with the site and the type, and we will look at adding it.