Documentation

API documentation

One base URL, five recognition endpoints. Built to be a drop-in for the NopeCHA recognition API.

Quickstart

  1. Create an account and copy your API key from the dashboard.
  2. Activate a plan. A key cannot solve anything without an active plan. Paid plans: checkout is coming soon.
  3. Send your first request. The base URL is https://api.solvecha.com.
curl
curl -X POST "https://api.solvecha.com/v1/recognition/textcaptcha" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"image_data": ["<BASE64 IMAGE>"]}'

# {"data": "1234567"}

The answer to a request is not in the first response. You get a job id. Fetch the answer with a second request, see Polling pattern.

Authentication

Send your key in one of these ways. The key is the same everywhere.

MethodWhere
key fieldJSON or form body of a POST request
Authorization: Bearer <key>Header, any request
Authorization: Basic <key>Header, any request. The raw key is accepted.
?key=<key>Query string, for GET requests

Keep your key secret. Send requests from your server, not from a browser page.

Endpoints

Method and pathPurpose
POST https://api.solvecha.com/v1/recognition/recaptchaCreate a reCAPTCHA grid job
POST https://api.solvecha.com/v1/recognition/textcaptchaCreate a text captcha job
POST https://api.solvecha.com/v1/recognition/geetestCreate a GeeTest slider job
POST https://api.solvecha.com/v1/recognition/funcaptchaCreate a FunCAPTCHA tile job
POST https://api.solvecha.com/v1/recognition/funcaptcha_matchCreate a FunCAPTCHA match job
GET https://api.solvecha.com/v1/recognition/<type>?id=<id>Fetch the answer of a job
GET https://api.solvecha.com/v1/statusPlan and quota of your key

Legacy paths

These work for older NopeCHA clients. The type field is required on the root path.

Method and pathPurpose
POST https://api.solvecha.com/Create a job. Put type in the body.
GET https://api.solvecha.com/?id=<id>&key=<key>Fetch the answer of a job
GET https://api.solvecha.com/status?key=<key>Plan and quota of your key

Request and response formats

Bodies are JSON or url-encoded forms. Multipart is not supported. The body limit is 4.5 MB. Images are base64 strings in the image_data array. On the v1 paths the type comes from the path, so you can omit type.

FieldMeaning
keyYour API key, if you do not send it in a header
typerecaptcha, textcaptcha, geetest, funcaptcha or funcaptcha_match
taskThe instruction text, for example bus. Up to 2,048 characters.
gridreCAPTCHA only: 3x3, 4x4 or 1x1
image_dataArray of base64 images. For form bodies use image_data[0], image_data[1] and so on.

reCAPTCHA (recaptcha)

For 3x3 and 4x4 send one image of the whole grid. For 1x1 send 1 to 9 tile images. The answer has one boolean per tile, left to right, top to bottom. For 1x1 it has one boolean per image.

Request
{"task": "bus", "grid": "3x3", "image_data": ["<BASE64 GRID>"]}
Answer
{"data": [false, true, false, true, true, false, false, false, false]}

Text captcha (textcaptcha)

Exactly one image and no task text. The answer is an array with one case-sensitive string.

Request
{"image_data": ["<BASE64 IMAGE>"]}
Answer
{"data": ["K7m2Q"]}

GeeTest slider (geetest)

Send a task and either one complete challenge screenshot, or two images: the background first, then the transparent puzzle piece. The answer is x, the offset as a percentage of the background image width.

Request
{"task": "slide", "image_data": ["<BASE64 BACKGROUND>", "<BASE64 PIECE>"]}
Answer
{"data": {"x": 42.5}}

FunCAPTCHA tile (funcaptcha)

Send the task text and one image with the 3x2 grid. The answer has one boolean per tile, in row-major order.

Request
{"task": "Pick the sheep", "image_data": ["<BASE64 GRID>"]}
Answer
{"data": [false, false, false, true, false, false]}

FunCAPTCHA match (funcaptcha_match)

Send the task text and one uncropped game image. The answer has one boolean per answer tile.

Request
{"task": "Match the number of rocks with the number on the left", "image_data": ["<BASE64 IMAGE>"]}

Create response

Every create request answers with a job id.

Create response
{"data": "1234567"}

Examples

Each example creates a reCAPTCHA 3x3 job and polls for the answer.

curl

curl
KEY=YOUR_API_KEY
BASE=https://api.solvecha.com
IMG=$(base64 -w0 grid.png)

ID=$(curl -s -X POST "$BASE/v1/recognition/recaptcha" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d "{\"task\":\"bus\",\"grid\":\"3x3\",\"image_data\":[\"$IMG\"]}" | jq -r .data)

sleep 1
curl -s "$BASE/v1/recognition/recaptcha?id=$ID" -H "Authorization: Bearer $KEY"

Python

Python (requests)
import base64, time, requests

BASE = "https://api.solvecha.com"
headers = {"Authorization": "Bearer YOUR_API_KEY"}

with open("grid.png", "rb") as f:
    image = base64.b64encode(f.read()).decode()

job = requests.post(f"{BASE}/v1/recognition/recaptcha", headers=headers,
                    json={"task": "bus", "grid": "3x3", "image_data": [image]}).json()

for _ in range(30):
    time.sleep(1)
    r = requests.get(f"{BASE}/v1/recognition/recaptcha", headers=headers, params={"id": job["data"]})
    if r.status_code == 409:  # error 14: not ready yet
        continue
    r.raise_for_status()
    print(r.json()["data"])
    break

Node

Node (fetch)
import { readFile } from "node:fs/promises";

const BASE = "https://api.solvecha.com";
const headers = { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" };
const image = (await readFile("grid.png")).toString("base64");

const job = await fetch(`${BASE}/v1/recognition/recaptcha`, {
  method: "POST",
  headers,
  body: JSON.stringify({ task: "bus", grid: "3x3", image_data: [image] }),
}).then((r) => r.json());

for (let i = 0; i < 30; i++) {
  await new Promise((r) => setTimeout(r, 1000));
  const res = await fetch(`${BASE}/v1/recognition/recaptcha?id=${job.data}`, { headers });
  if (res.status === 409) continue; // error 14: not ready yet
  console.log((await res.json()).data);
  break;
}

Polling pattern

  1. POST creates the job and returns {"data": "<id>"}.
  2. GET the same endpoint with id. If the job is not ready, you get error 14, Incomplete job.
  3. Poll every 1 second, for up to about 30 seconds. When the job is ready, the GET returns the answer.

Polling requests are rate limited too. Do not poll faster than once per second per job.

Error codes

CodeMessagev1 HTTP status
9Unknown error500
10Invalid request400
11Rate limit reached429
14Incomplete job409
15Invalid key401
16Out of credit403

The /v1/ paths use the HTTP status above and this body:

v1 error
{"error": 14, "code": 14, "message": "Incomplete job", "type": null}

Legacy paths answer with HTTP 200 and this body:

Legacy error
{"error": 14, "message": "Incomplete job"}

Status endpoint

GET https://api.solvecha.com/v1/status returns the state of your key.

Status
{
  "plan": "Basic",
  "status": "Active",
  "credit": 19873,
  "quota": 20000,
  "duration": 86400,
  "lastreset": 1760486400,
  "ttl": 41200,
  "subscribed": 0,
  "current_period_start": 1760000000,
  "current_period_end": 1762600000
}
FieldMeaning
planName of the plan
statusActive or Expired
creditSuccessful solves left in the current daily window
quotaSuccessful solves per day for the plan
durationLength of the quota window in seconds
lastresetUnix time of the last quota reset
ttlSeconds until the next reset
subscribedKept for compatibility with NopeCHA
current_period_startUnix time when the plan period began
current_period_endUnix time when the plan period ends

The values above are an example.

Quota and rate limits

  • Only successful solutions count against the daily quota.
  • The quota resets every day at 00:00 UTC.
  • No plan means no solving.
  • Without quota, requests fail with error 16 until the reset.
PlanSuccessful solves per dayCreate requests per minute
Starter2,00060
Basic20,000300
Professional80,000600
Enterprise200,0001200

Migrating from NopeCHA

Replace the base URL. Use your Solvecha key. Nothing else changes: paths, fields, answers and error codes follow the NopeCHA recognition API.

Before and after
# before
BASE = "https://api.nopecha.com"
KEY = "YOUR_NOPECHA_KEY"

# after
BASE = "https://api.solvecha.com"
KEY = "YOUR_API_KEY"

Differences to know: only JSON and url-encoded form bodies are supported, and a key needs an active plan to solve.