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.
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:
| Method | Example |
|---|---|
| Header (recommended) | X-API-Key: sk_โฆ |
| Query string | ?key=sk_โฆ |
| Body field | {"apikey": "sk_โฆ"} |
Inside data | {"data": {"apikey": "sk_โฆ"}} |
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
0means unlimited concurrency.
Submit a challenge
Request body
| Field | Type | Description |
|---|---|---|
data | object | The raw challenge object from hCaptcha. |
data.request_type | string | Challenge type โ see below. |
data.tasklist | array | Tasks, each with a datapoint_uri. |
data.requester_question | object | Prompt 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
| State | Response |
|---|---|
| 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
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 }
] }
request_type fails the job with
unsupported request_type โ and refunds your credit.
Error codes
| Status | Meaning | What 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"); }