API documentation

Everything you need to send your first challenge.

Quickstart

The API is wire-compatible with the standard recognition protocol: you POST a challenge, get a job id back immediately, then GET that id until the answer is ready. Two calls, no websockets.

Base URL โ€” every path below is relative to this host.

1. Submit the challenge

curl -X POST "$BASE/v1/recognition/hcaptcha" \
  -H "X-API-Key: sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"data": {"request_type": "image_label_binary", "tasklist": [...]}}'

# โ†’ {"data": "9f2c1e40-5b7a-4c3d-8e11-2a6f0b9d4c88"}

2. Poll until it resolves

curl "$BASE/v1/recognition/hcaptcha?id=9f2c1e40-โ€ฆ" \
  -H "X-API-Key: sk_your_key"

# still working โ†’ {"error": "pending"}
# done         โ†’ {"data": [[true, false, true, โ€ฆ]]}

Poll every 200โ€“500 ms. Most challenges resolve in well under a second.

Authentication

Send your key in whichever way suits your client โ€” all four are accepted:

MethodExample
Header (recommended)X-API-Key: sk_โ€ฆ
Query string?key=sk_โ€ฆ
Body field{"apikey": "sk_โ€ฆ"}
Inside data{"data": {"apikey": "sk_โ€ฆ"}}
Treat your key like a password. It spends your credits and cannot be rotated by you โ€” ask an administrator if it leaks.

Keys are issued from the admin panel. Track usage on your dashboard.

Credits & threads

Credits

  • 1 credit = 1 solve. One credit is deducted the moment a job is accepted.
  • Failures are refunded. If the solver errors out, the credit returns to your balance โ€” you only pay for answers.
  • At zero credits, submissions are rejected with 402 Payment Required.
  • Keys marked unlimited (โˆž in the dashboard) are never charged.

Thread limit

  • Each key has a maximum number of jobs that may run at the same time.
  • Submitting past that limit returns 429 Too Many Requests โ€” the request is not queued.
  • A slot frees up as soon as a job finishes, successfully or not.
  • A limit of 0 means unlimited concurrency.
Sizing tip. Run roughly as many worker threads as your key's limit. Higher just produces 429s; lower leaves throughput on the table.

Submit a challenge

POST/v1/recognition/hcaptcha

Request body

FieldTypeDescription
dataobjectThe raw challenge object from hCaptcha.
data.request_typestringChallenge type โ€” see below.
data.tasklistarrayTasks, each with a datapoint_uri.
data.requester_questionobjectPrompt text, keyed by language (en preferred).

datapoint_uri accepts both an external URL and an inline data: URI โ€” remote images are downloaded server-side.

Response

{ "data": "9f2c1e40-5b7a-4c3d-8e11-2a6f0b9d4c88" }

The string is your job id. It stays valid for 10 minutes.

Poll for the result

GET/v1/recognition/hcaptcha?id=<job_id>
StateResponse
pending{"error": "pending"}
done{"data": <answer>}
error{"error": "<message>"}

The shape of data depends on the challenge type โ€” see the next section. An unknown job id returns 404.

Health check

GET/health

Unauthenticated. Useful for load balancers and uptime monitors.

{ "status": "ok", "jobs": 3 }

Challenge types

image_label_binary

Yes/no per image โ€” "select each image containing a bus".

// one boolean per task, wrapped in an outer array
{ "data": [[true, false, true, true, false]] }

image_drag_drop

Drag each entity onto its target. Coordinates are percentages of the background image; w and h echo the entity size in pixels.

{ "data": [[
  {
    "entity_id": "e0",
    "entity_name": "e0",
    "x": 62.4, "y": 38.1,
    "w": 63,   "h": 63
  }
]] }

area_select ยท image_label_area_select

A single point or region per image, again in percentages.

{ "data": [
  { "x": 47.2, "y": 51.8, "w": 0, "h": 0 }
] }
Any other request_type fails the job with unsupported request_type โ€” and refunds your credit.

Error codes

StatusMeaningWhat to do
401 Invalid or missing API key Check the key; it may have been disabled.
402 No credits left Top up before retrying โ€” retries won't succeed.
404 Job not found The id is wrong or older than 10 minutes.
429 Thread limit reached Wait for an in-flight job, then retry.

Error bodies always carry a detail string explaining the cause.

Code examples

Python

import time, requests

BASE = "$BASE"
KEY  = "sk_your_key"
H    = {"X-API-Key": KEY}

def solve(challenge, timeout=30):
    r = requests.post(f"{BASE}/v1/recognition/hcaptcha",
                      json={"data": challenge}, headers=H)
    r.raise_for_status()
    job_id = r.json()["data"]

    deadline = time.time() + timeout
    while time.time() < deadline:
        res = requests.get(f"{BASE}/v1/recognition/hcaptcha",
                           params={"id": job_id}, headers=H).json()
        if "data" in res:
            return res["data"]
        if res.get("error") != "pending":
            raise RuntimeError(res["error"])
        time.sleep(0.3)

    raise TimeoutError("solve timed out")

Node.js

const BASE = "$BASE";
const KEY  = "sk_your_key";
const H    = { "X-API-Key": KEY, "Content-Type": "application/json" };

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function solve(challenge, timeoutMs = 30000) {
  const res = await fetch(`${BASE}/v1/recognition/hcaptcha`, {
    method: "POST", headers: H,
    body: JSON.stringify({ data: challenge }),
  });
  if (!res.ok) throw new Error(`submit failed: ${res.status}`);
  const { data: jobId } = await res.json();

  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const out = await (await fetch(
      `${BASE}/v1/recognition/hcaptcha?id=${jobId}`, { headers: H })).json();
    if (out.data !== undefined) return out.data;
    if (out.error !== "pending") throw new Error(out.error);
    await sleep(300);
  }
  throw new Error("solve timed out");
}
Handling 429. Both examples throw on a thread-limit rejection. In production, catch it and retry with a short backoff instead of failing the whole task.