Legibl docs PHP 0.1.0-dev · Python 0.1.0

HTTP API

Five endpoints, all POST, all JSON, all authenticated by a clientKey in the request body. This is Legibl's own API, compatible with the task-based protocol the category converged on — an existing client of a major solver migrates by changing its base URL and key.

Implementation status

The task parser and the error envelope are implemented in the gateway. The public HTTP endpoints are not yet deployed, and no API version identifier has been assigned. This page documents the contract the SDKs are written against; treat it as the specification rather than as a live service until Versions says otherwise.

Conventions

Base URLhttps://api.legibl.io (override with LEGIBL_SERVICE_URL)
MethodPOST for every endpoint
Content typeapplication/json
AuthenticationclientKey in the request body. There is no header auth.
ScopingTasks, reports and corpus entries belong to the key that created them. Another key's taskId is indistinguishable from one that does not exist.

The errorId / errorCode envelope

Every response carries errorId. Zero means success and the rest of the object is the payload. One means failure, and errorCode carries the meaning while errorDescription carries the detail.

{ "errorId": 0, "taskId": "b1f0…" }
{
  "errorId": 1,
  "errorCode": "ERROR_KEY_DOES_NOT_EXIST",
  "errorDescription": "Account authorization key not found"
}
Switch on the code, read the description

Codes are stable even when the wording changes. Descriptions are for humans and may be extended with detail after a colon. Never parse the description.

POST /createTask

Submits a captcha for solving and returns a task id.

Request

FieldTypeRequiredNotes
clientKeystringyesYour API key.
taskobjectyesSee task types below. Missing or non-object yields ERROR_TASK_ABSENT.
callbackUrlstringnoThe task result is POSTed here when it resolves, with a payload identical to getTaskResult. Polling remains available alongside it.

Task types

reCAPTCHA v2 and v2 invisible

{
  "clientKey": "your-key",
  "task": {
    "type": "RecaptchaV2TaskProxyless",
    "websiteURL": "https://example.test/login",
    "websiteKey": "6Lc…",
    "isInvisible": true
  }
}

websiteURL and websiteKey are required non-empty strings; either missing yields ERROR_INVALID_TASK_DATA. isInvisible selects the recaptcha_v2_invisible type and accepts true, "true" or 1, because all three appear in vendor client libraries in the wild. Anything else is false.

Cloudflare Turnstile

{
  "clientKey": "your-key",
  "task": {
    "type": "TurnstileTaskProxyless",
    "websiteURL": "https://example.test/login",
    "websiteKey": "0x4AAA…",
    "action": "login",
    "cdata": "opaque-string"
  }
}

action and cdata are optional and are forwarded when present. Both alter the token Cloudflare issues, so dropping them yields a token the target rejects — send them whenever the widget carries data-action or data-cdata.

Image OCR

{
  "clientKey": "your-key",
  "task": {
    "type": "ImageToTextTask",
    "body": "iVBORw0KGgoAAAANSUhEUg…"
  }
}

body is the image, base64-encoded, whitespace tolerated. It is decoded at the door: invalid base64 or an empty decode yields ERROR_INVALID_TASK_DATA and no job is created, because paying a backend to discover the same thing is waste.

Accepted type spellings

Vendors disagree on spelling while agreeing on everything else. Legibl accepts their vocabulary and does not require its own. This tolerance is one-directional: these are all accepted on input; solutions come back in the shape each type's clients expect.

CaptchaAccepted task.type values
reCAPTCHA v2 RecaptchaV2Task, RecaptchaV2TaskProxyless
Turnstile TurnstileTask, TurnstileTaskProxyless, AntiTurnstileTask, AntiTurnstileTaskProxyLess (CapSolver's capital L), AntiTurnstileTaskProxyless
Image OCR ImageToTextTask

Anything else yields ERROR_TASK_NOT_SUPPORTED and creates no job. hCaptcha, DataDome, Kasada and PerimeterX task types are rejected by this rule — see Supported captcha types.

Proxy fields

A task may carry proxyType, proxyAddress, proxyPort, proxyLogin and proxyPassword. The convention is that the …Task variant carries a proxy and the …TaskProxyless variant does not; a result bound to the requesting address has to be obtained from that address.

No currently supported type is address-bound, so no currently supported type requires a proxy. Credentials are carried through verbatim and never inspected, logged, captured to the corpus, or shown in a dashboard.

Responses

{ "errorId": 0, "taskId": "b1f0…" }
FailureCode
Key missing or unrecognisedERROR_KEY_DOES_NOT_EXIST
Insufficient balanceERROR_ZERO_BALANCE
task missing, or not an object, or no typeERROR_TASK_ABSENT
Type outside the supported setERROR_TASK_NOT_SUPPORTED
Required field missing or malformedERROR_INVALID_TASK_DATA
Address-bound type submitted with no proxyERROR_PROXY_REQUIRED
No capacity right nowERROR_NO_SLOT_AVAILABLE

POST /getTaskResult

Returns the current state of a task. Poll it, or supply a callbackUrl on creation and skip polling.

{ "clientKey": "your-key", "taskId": "b1f0…" }

Still working

{ "errorId": 0, "status": "processing" }

Ready

{
  "errorId": 0,
  "status": "ready",
  "solution": { "gRecaptchaResponse": "03AGdBq26…" }
}

The solution object is shaped by captcha type. Vendors disagree on the key even while agreeing on everything else, so Legibl emits the spelling each type's clients already expect rather than inventing a uniform one everybody would have to special-case.

Captcha typeSolution keyValue
recaptcha_v2, recaptcha_v2_invisiblegRecaptchaResponseThe reCAPTCHA token.
turnstiletokenThe Turnstile token.
image_ocrtextThe transcribed text.

The SDKs read gRecaptchaResponse, then token, then text, and treat a ready result carrying none of them as a failure.

Failures

ConditionCode
Every eligible backend failed or exhausted retriesERROR_CAPTCHA_UNSOLVABLE
Task unresolved past its maximum lifetimeERROR_TIMEOUT
Unknown task id, or one owned by a different keyERROR_NO_SUCH_CAPCHA_ID
The misspelling is deliberate

ERROR_NO_SUCH_CAPCHA_ID is spelled exactly that way across every vendor in this category. Legibl keeps the misspelling so that clients written against any of them work here without a lookup table.

POST /getUsage

{ "clientKey": "your-key" }
{
  "errorId": 0,
  "usage": [
    { "captchaType": "recaptcha_v2", "targetDomain": "shop.example.com",
      "volume": 1420, "spend": "0.852000" }
  ],
  "totalVolume": 1420,
  "totalSpend": "0.852000"
}

Your own traffic, broken down by captcha type and target domain. Scoped to the calling key at the query, so one account can never see another's.

Money is a decimal string, not a number. A float renders as 0.30000000000000004 often enough to matter, and this figure ends up next to an invoice. Parse it as a decimal.

Which backend served a solve is deliberately absent. That is our supplier relationship rather than yours, and publishing it would also publish our margin.

GET /metrics

Operational metrics in Prometheus text format: job counts by status and captcha type, solve-duration histogram, acceptance verdicts, refused reservations, webhook outcomes and backend health transitions.

curl -H "Authorization: Bearer $LEGIBL_METRICS_TOKEN" https://api.legibl.io/metrics

Off unless you configure a scrape token, and 404 rather than 401 when the token is wrong — an unauthenticated caller learns nothing about whether the endpoint exists on a given host.

Not merely internal-by-convention: the labels name the upstream backends, so an open /metrics publishes which vendors serve your traffic and in what proportion. Defaulting to off means a deployment that never thinks about this does not accidentally announce its supply chain.

Target domain is deliberately absent from the labels. It is unbounded — one series per site — and unbounded label values are the usual way to take down a metrics backend. Use getUsage for per-target figures.

POST /getBalance

{ "clientKey": "your-key" }
{ "errorId": 0, "balance": 42.5 }

This is your Legibl balance, not any upstream vendor's. Card-purchased credit is held briefly before it becomes spendable, so an amount can be credited and not yet available; crypto-purchased credit is spendable immediately, because there is nothing to charge back.

POST /reportCorrect

POST /reportIncorrect

Vendor spellings of the report endpoints

Anti-Captcha names these with the captcha type appended, and real integrations call them by that name. All four aliases are served and behave identically to the short forms:

POST /reportCorrectRecaptcha
POST /reportIncorrectRecaptcha
POST /reportCorrectImageCaptcha
POST /reportIncorrectImageCaptcha

If you are migrating, check which name your code calls. Against a gateway that serves only the short form, these 404 — and the failure is silent. The solve already succeeded, nothing raises, and your accept rate simply stops moving. That figure is what makes cost per accepted solve a real number rather than list price wearing a disguise, so it is the last signal that should fail quietly.

{ "clientKey": "your-key", "taskId": "b1f0…" }
{ "errorId": 0 }

reportCorrect records accepted = true against the task. reportIncorrect records accepted = false and attributes the failure to the backend that produced the answer. Reporting the same task twice keeps the first outcome and still answers errorId: 0.

Why this is not optional

Acceptance is the only signal that distinguishes “a backend returned an answer” from “the answer worked”. A backend answering promptly with tokens the target rejects is worse than one that is plainly down, because nothing else notices. If your acceptance-report rate falls below the configured threshold, an operator alert names your key.

The SDKs' interception path observes acceptance directly — it resubmits the request and sees whether the target took it — so callers on Legibl\curl_exec or the Python transports do not need to call these endpoints. Callers driving the API themselves do.

Error codes

The complete table. These are the industry-standard strings, which is the entire migration argument: a client written against another vendor handles ours without a lookup table.

CodeDescriptionRetry?Corrective action
ERROR_KEY_DOES_NOT_EXIST Account authorization key not found never Fix the key. A client that retries an auth failure retries forever — see troubleshooting.
ERROR_ZERO_BALANCE Account has insufficient funds no Top up. Check whether a card top-up is still within its hold period.
ERROR_NO_SUCH_CAPCHA_ID Task not found or not owned by this key no Check the task id and that you are presenting the key that created it.
ERROR_CAPTCHA_UNSOLVABLE The captcha could not be solved new task Terminal for that task. Submit a fresh one; a persistent pattern on one target is worth reporting.
ERROR_NO_SLOT_AVAILABLE No capacity is currently available yes Back off and retry. Several distinct backend conditions collapse into this one because from your side they mean the same thing.
ERROR_TASK_ABSENT The task object is missing no Send a task object with a type.
ERROR_TASK_NOT_SUPPORTED The task type is not supported no Use one of the four supported types.
ERROR_INVALID_TASK_DATA The task object is malformed no A required field is missing, empty, the wrong type, or — for image tasks — not decodable base64.
ERROR_PROXY_REQUIRED This captcha type requires a caller-supplied proxy no Resubmit with proxy fields on the non-Proxyless variant.
ERROR_RATE_LIMIT Too many requests yes Back off.
ERROR_TIMEOUT The task exceeded its maximum lifetime new task Terminal for that task.
ERROR_UNKNOWN An unexpected error occurred yes Retry once, then report it. An internal failure is never dressed up as a client error code, so seeing this means something genuinely unclassified happened.

Compatibility, stated honestly

The task-based protocol is a genuine de-facto standard — createTask / getTaskResult, shared task-type strings, a shared error envelope — but there is no specification, no conformance suite and no governing body. It is convergence by copying: stable in the common path and divergent at the edges. Compatibility with it is a property Legibl states per version, not an open-ended commitment.