A CAPTCHA-solving API is not a function that returns an answer. It is a job queue you talk to over HTTP: you hand it a task, it hands back an ID, and you return later to collect a token that may or may not exist yet. Most integration bugs in this space come from treating that queue as a synchronous call.
Key takeaway: solving is asynchronous because the token is earned, not computed. Submit a task, poll on a slow cadence until the "not ready" signal stops, then inject the returned token into the exact field the page reads. Get those three stages right — and the proxy and concurrency model around them — and the rest is plumbing.
Why solving has to be asynchronous
One distinction between challenge types explains the whole protocol shape.
A classic image CAPTCHA is a classification problem. You send pixels, a model reads them, you get characters back — nothing external is involved. CaptchaAI publishes sub-0.5-second times on image CAPTCHAs across 27,500+ variants, about what you would expect from a straight inference call.
A token challenge is a different animal. A reCAPTCHA v2 response token is not produced by you or by a solver — it is issued by Google's own endpoint after an interaction it deems acceptable, from a network context it deems acceptable. Something has to actually drive a browser session through the challenge, which is wall-clock work on someone else's infrastructure. CaptchaAI's published envelope for reCAPTCHA v2 is >99.5% success in under 60 seconds; v3 is >99% in under 4 seconds; Cloudflare Turnstile is published at 100% in under 10 seconds.
So one API covers work that takes half a second and work that takes the better part of a minute. No sane HTTP client holds a connection open across that range. Hence: submit, poll, inject.
Stage 1 — Submit the task to in.php
You POST to the endpoint host with your 32-character key, a method that declares the challenge
type, and whatever parameters that type needs.
curl -X POST "https://ocr.captchaai.com/in.php" \
-d "key=YOUR_API_KEY" \
-d "method=userrecaptcha" \
-d "googlekey=6Lc_SITE_KEY_FROM_THE_PAGE" \
-d "pageurl=https://staging.example.com/checkout" \
-d "json=1"
method is the switch that changes everything else. base64 takes an encoded image in a body
parameter and needs no page context at all. userrecaptcha needs the site key and the page URL.
reCAPTCHA v3 adds version=v3, an action matching what the site's own JavaScript passes, and a
min_score. Read the parameter list for your specific type rather than pattern-matching from
another one — a v3 task submitted without its action comes back with a token the site scores
badly, and that failure looks like a solver problem when it is a parameter problem.
With json=1 the reply is structured:
{ "status": 1, "request": "2122988149" }
That request value is your task ID, and status: 1 means accepted, not solved. Store the ID with a
submission timestamp — you need elapsed time to enforce your own deadline in stage 2.
Stage 2 — Poll res.php until it stops saying not-ready
curl "https://ocr.captchaai.com/res.php?key=YOUR_API_KEY&action=get&id=2122988149&json=1"
While the task is still in flight:
{ "status": 0, "request": "CAPCHA_NOT_READY" }
When it lands:
{ "status": 1, "request": "03AGdBq26...token..." }
Two things trip people up here.
The HTTP status code is not your signal. A not-ready response is a perfectly successful HTTP
200. The status field in the body is authoritative. If your client treats 200 as "done", you will
inject the literal string CAPCHA_NOT_READY into a form field and spend an afternoon wondering why
the site rejects it.
CAPCHA_NOT_READY is spelled that way on purpose. The missing T is a legacy artifact of the
protocol that every implementation now preserves for compatibility. Match the string exactly; do
not "fix" it in your constants file.
A poll loop with a deadline, in Python:
import time, requests
BASE = "https://ocr.captchaai.com"
KEY = "YOUR_API_KEY"
TERMINAL = {"ERROR_UNSOLVABLE", "ERROR_ZERO_BALANCE", "ERROR_WRONG_USER_KEY"}
def poll(task_id, deadline_s=120, first_wait=10, interval=5):
time.sleep(first_wait) # nothing is ready instantly
started = time.time()
while time.time() - started < deadline_s:
r = requests.get(f"{BASE}/res.php", params={
"key": KEY, "action": "get", "id": task_id, "json": 1
}, timeout=15).json()
if r["status"] == 1:
return r["request"] # the token
if r["request"] != "CAPCHA_NOT_READY":
raise RuntimeError(f"terminal: {r['request']}") # ERROR_UNSOLVABLE etc.
time.sleep(interval)
raise TimeoutError(f"task {task_id} exceeded {deadline_s}s")
The documented cadence is roughly five seconds, and there is no reward for beating it. Polling every 200ms does not make a browser farm finish sooner; it multiplies your request volume against a result that is not there yet. Sleep before the first poll too — for a v2 task, checking at t=0 is wasted.
Set the deadline per challenge type, not globally. The published envelopes give you a starting budget: a little slack over 4s for v3, something well north of 60s for v2. Treat those as planning figures, not a guarantee any given task will hit them.
The other half of this loop is classifying what comes back. CAPCHA_NOT_READY is transient and you
keep waiting. ERROR_UNSOLVABLE is terminal for that task — resubmit at most once, then let it
fail. ERROR_ZERO_BALANCE is terminal for every task and should page someone rather than trigger
a retry storm. Retry and backoff strategy gets its own treatment in
running a CAPTCHA API in production; the minimum here is that your
code can tell the three categories apart.
Stage 3 — Inject the token where the page actually reads it
A token you deliver incorrectly is a token you paid for and wasted. Where it goes depends on the challenge:
- reCAPTCHA v2: the page reads a
g-recaptcha-responsefield. In a real browser session you set the value of the hidden textarea; if you are posting the form directly, addg-recaptcha-response=<token>to the body. - reCAPTCHA v3: there is no standard field. The site's own JavaScript decides what to call the parameter it sends to its backend. Watch the network tab on a manual run and copy that name.
- Turnstile:
cf-turnstile-response, same pattern as v2. - Cloudflare Challenge: this one returns a clearance cookie and a user agent. You must send both, and your requests must use that exact user agent string. A clearance cookie presented under a different UA is worthless — the pairing is the point.
If you are driving a real browser, set the value and then dispatch whatever event the page's listener
is bound to. Assigning .value on a hidden input does not always fire the framework's change
handler, and a form that never learned about the token behaves like a form with no token.
JSON or plain text — decide once, be strict
Omit json=1 and you get the legacy plain-text format: a bare CAPCHA_NOT_READY, or a
pipe-delimited OK|<token> on success. It exists because the protocol predates everyone's JSON
habit, and because a great deal of working integration code still parses it.
For new code, always send json=1. String-prefix parsing is how you end up with a token that has a
stray OK| glued to the front. Plain text is worth knowing for one reason: when porting an old
integration you can leave the parser alone during cutover and change format later, as a separate
revertible step.
Proxy per task, and when it matters
The API accepts proxy and proxytype parameters so the solve happens through your egress rather
than the provider's.
Whether you need it depends on the challenge type again. An image CAPTCHA has no network context at all — proxyless is correct and faster. Score-based challenges are different: reCAPTCHA v3 and its relatives fold IP and ASN reputation into the score they assign, so a token earned from a datacenter range and presented from your residential egress carries a visible mismatch, and the site may reject a token the solver produced perfectly well.
The rule of thumb: if the challenge scores the network, solve from the network you will submit from. Expect proxied solves to run slower — you have added a hop.
Concurrency is the dial that matters
CaptchaAI prices concurrent threads, not solves: a thread is one in-flight task, solves per thread are unlimited, and there are no per-CAPTCHA fees, daily caps or per-type surcharges. The sizing question becomes "how many do we need in flight at once", not "how many this month".
The arithmetic follows from stage 2: throughput ceiling is roughly threads divided by average solve time. Fifty threads against work averaging 30 seconds is about 1.7 solves per second — call it 100 a minute — while the same fifty threads against sub-second image work is a completely different number. That is derived from the published latency envelopes above, not a benchmark of your workload; measure your own average once live.
Published tiers run BASIC at $15/month for 5 threads, ADVANCE at $90/month for 50 threads (from around $0.09 per 1,000) and ENTERPRISE at $300/month for 200 threads (from around $0.030 per 1,000), with STANDARD, PREMIUM, CORPORATE and VIP steps filling in the ladder.
The practical consequence: put a semaphore sized to your thread count in your client. Submitting more concurrent tasks than you have threads does not buy throughput.
Why migration is mostly a host swap
The in.php / res.php shape is the legacy 2Captcha-style protocol, and CaptchaAI implements it
alongside a compatibility emulator that mimics other services' hosts. For a working integration that
usually means changing a base URL and a key, leaving parameter names, response parsing and error
strings intact.
"Usually" is doing real work there. Before cutting over, diff the parameter list for each method
you use, confirm the response format your parser expects, and check your error-string constants
match. Then run shadow traffic — submit a slice of live tasks to both endpoints and compare outcomes
before moving the switch. Protocol compatibility makes this a small change; it is not a reason to
skip verifying it.
Keep it on the right side of the line
Everything above assumes a legitimate target: your own staging environment, a QA suite you own,
accessibility testing, uptime monitoring, or data collection you are permitted to perform. Honour
robots.txt, stay inside published rate limits, and read the terms of service of the site you are
automating. Build the rate limiter before the solver client — it is the part that keeps the work
defensible. Solving CAPTCHAs to attack authentication, take over accounts, create accounts in bulk,
commit payment fraud or push spam is out of bounds, and no integration guide changes that.
FAQ
Why does res.php keep returning CAPCHA_NOT_READY?
Usually because the task genuinely is not finished — reCAPTCHA v2 is published at under 60 seconds,
so a client that gives up at 15 sees nothing else. If it persists well past the published envelope
for that type, check the pageurl and site key you submitted; a task pointed at the wrong page can
grind without ever producing a usable token.
Should I poll faster than every five seconds? No. The ~5s cadence is the documented rhythm and tighter polling does not shorten the solve — it only adds request volume. If latency is your problem, submit earlier in your flow rather than polling harder.
Do I need a proxy for every task?
Only where the challenge scores your network context. Score-based challenges like reCAPTCHA v3 care
about the IP the token was earned from, so pass proxy and proxytype there. Image classification
has no network context to match and runs faster without one.
How long is a returned token good for? Short. Challenge tokens are single-use and expire in minutes, per the challenge provider's rules rather than the solver's — check the documentation for the type you are handling. Send the token straight into the request that needs it instead of parking it in a queue.
Closing
The pattern is small once you see it: one POST that returns an ID, a patient loop that respects the five-second rhythm and knows a transient error from a terminal one, and an injection step that puts the token in the exact field the page reads. Size threads to concurrency rather than volume, proxy the challenges that score your network, and keep the targets legitimate.
The full parameter reference for each method, the documented error codes, and working examples in
cURL, Python, Node.js, PHP, Go, Java, .NET, Ruby, Rust and Postman are at
getcaptchaai.com.