API documentation
Two ways to pass hCaptcha. Ask the Token API for a ready P1_ token, or send a challenge to the Recognition API and get back where to click. Both are JSON over HTTPS.
| Base URL | https://api.ninetile.io/v1 |
| Auth | Authorization: Bearer nt_live_… |
| Content type | application/json |
| Billing | Prepaid US dollars, charged only for work that succeeds |
Quickstart
- Create an account and add balance in the dashboard.
- Create a key in API keys. It is shown once.
- Ask for a token.
?wait=60holds the connection until the solve finishes (1 to 90 seconds).
curl "https://api.ninetile.io/v1/solves?wait=60" \
-H "Authorization: Bearer nt_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"hcaptcha","sitekey":"f5ab1c2d-7e8f-4a9b-b1c2-d3e4f5a6b7c8","url":"https://target.example/login"}'Put token into the form's h-captcha-response field and send the request with the solve's user_agent as your User-Agent. A token is single use and expires within minutes, so submit it right away.
Authentication
Keys look like nt_live_ followed by 32 characters. Send one on every request as Authorization: Bearer <key> (x-api-key also works). Create, limit and revoke keys in the dashboard. Only a hash is stored, so a lost key cannot be recovered, only replaced.
- Permissions: a key can be limited to the Token API, the Recognition API, or both.
- IP allow-list: add the addresses a key may be used from. Requests from anywhere else get
403 forbidden. - Spend limit: a key stops with
402 key_credit_limit_exceededonce it has spent its limit.
Responses carry X-Request-Id. Quote it when you contact support.
Pricing and billing
| Call | Price |
|---|---|
| A solve that returns a token | $0.75 per 1,000 ($0.00075 each) |
| A recognition that returns an answer | $0.30 per 1,000 ($0.0003 each) |
| Failed, cancelled or expired solves; refused recognitions | Free |
Creating a solve holds its price against your balance, so two requests can never spend the same dollar. A success turns the hold into the charge; any other outcome releases it. GET /v1/me shows balance_usd and available_usd (the balance after holds).
Token API
POST /v1/solves
| Field | Description |
|---|---|
type * | hcaptcha or hcaptcha_enterprise |
sitekey * | The UUID in the widget's data-sitekey. A reCAPTCHA key (6L…) is refused. |
url * | The page the challenge appears on. |
rqdata | The page's rqdata, when it provides one. Send it fresh for every challenge. |
user_agent | The user agent of the browser that loaded the page. |
proxy | http://user:pass@host:port, or an object with host, port, scheme, username, password. |
Proxies. Without proxy, the solve uses Ninetile's rotating residential pool, with a fresh exit address per solve. With proxy, it uses yours: choose one that keeps a single exit address for the whole exchange if the target checks that the token is submitted from the same address. Only http and https proxies are supported, and the host must be public.
Answers 202 while the solve is running and 200 once it has finished (with ?wait). The solve object:
{
"id": "solve_01HQF7K3JKWZX…",
"object": "solve",
"type": "hcaptcha",
"status": "solved", // pending | solving | solved | failed | cancelled | expired
"sitekey": "f5ab1c2d-…",
"url": "https://target.example/login",
"token": "P1_eyJ0eXAi…", // null until solved; cleared after 7 days
"user_agent": "Mozilla/5.0 …",
"error": null, // or { code, message, reason, retryable }
"charged_usd": 0.00075, // 0 unless solved; null while running
"created_at": "2026-11-01T07:24:22Z",
"started_at": "…", "finished_at": "…",
"queue_ms": 12, "resolve_ms": 8100
}Other calls
GET /v1/solves/{id}[?wait=N]returns200once finished,202while running.GET /v1/solveslists your solves, newest first. Query:limit(1 to 100),starting_after,status,type.DELETE /v1/solves/{id}cancels a running solve. Never charged.POST /v1/feedbackreports up to 500 verdicts at once:{"feedback":[{"solve_id","outcome","reason"?}]}with outcomeaccepted,rejected,unknown,unusedorerror.POST /v1/solves/{id}/feedbackreports one.GET /v1/meshows your balance and limits.GET /v1/usage?days=7shows spend per day.
A token can pass hCaptcha and still be refused by your target. Reporting rejected tokens through feedback is the only way we learn about it.
Solve failures
A request that was accepted can still end without a token. That is not an HTTP error: the solve comes back with status failed, cancelled or expired and an error object. Nothing is charged. Retry the retryable ones by creating a new solve.
| error.code | Retry | What happened |
|---|---|---|
challenge_not_loaded | yes | hCaptcha did not serve a challenge. reason sitekey_rate_limited means it answered 429. |
token_not_granted | yes | The challenge was answered but no token was issued. |
challenge_expired | yes | hCaptcha expired the challenge mid-solve. |
proxy_error | yes | Your proxy could not be used (or, without a proxy, our network could not reach hCaptcha). |
vision_error | yes | The challenge was served but our answers were rejected. |
unsolvable_variant | yes | A puzzle family we cannot solve yet. hCaptcha draws at random, so a retry usually works. |
capacity_exhausted | yes | Briefly at capacity. Retry in a few seconds. |
internal_error | yes | Something failed on our side. |
cancelled | no | You cancelled it. |
expired | yes | The deadline passed, or the service restarted mid-solve. |
Recognition API
For clients that run their own browser: send one challenge's images and instruction, get the answer back. No token, no polling. Each answered call costs $0.0003; a puzzle we cannot answer is 422 recognition_failed and free.
POST /v1/recognize
| Field | Description |
|---|---|
type * | hcaptcha (grid: pick the matching tiles) or hcaptcha_area_select (click on one image) |
task * | The instruction in English, up to 500 characters. |
image_data | Base64 images (bare or data:image/…;base64,). Grid: 1 to 27 tiles in order. Area select: exactly 1. |
image_urls | The same as URLs. Only https://*.hcaptcha.com addresses are fetched. |
image_examples | 1 to 3 example images shown with the instruction. |
Images are PNG, JPEG, WebP or GIF, up to 1.5 MiB and 1 megapixel each. Drag challenges, and any challenge you would rather send exactly as the widget gave it, use the full form: {"data": {"request_type", "requester_question": {"en"}, "tasklist": […]}} with image_label_binary, image_label_area_select or image_drag_drop.
curl https://api.ninetile.io/v1/recognize \
-H "Authorization: Bearer nt_live_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"type":"hcaptcha","task":"Select all bicycles","image_data":["<base64>", … 9 tiles]}'
{ "id": "rec_01K7Q2…", "data": [true, false, false, true, false, false, false, false, false], "charged_usd": 0.0003 }- Grid:
datais one boolean per tile, in the order you sent them.truemeans click it. - Area select:
datais{"x","y","w":0,"h":0}in percent of the image: click atx, y.pointslists every click. - Drag:
dataholds, per task, each piece'sentity_idand box in percent. Drop its centre atx + w/2, y + h/2.
POST /v1/recognize/outcome with {"id","result":"solved"|"failed"} is optional and never changes billing.
Chrome extension
The extension reads the challenge in your browser, asks Ninetile and clicks for you, with the same key and balance as the API. Create a key with Recognition access, open the extension's popup, paste the key and press Save & test. Its requests appear in your dashboard as extension requests, charged like Recognition API calls. It also answers video challenges, which the HTTP API does not take.
Switching from another service
Point your existing client at one of these base URLs and use your nt_live_ key as the key. Solves bill, rate-limit and appear in your dashboard like native ones. Only hCaptcha is served.
| Coming from | Base URL |
|---|---|
2Captcha (in.php, res.php) | https://api.ninetile.io/compat/2captcha |
| CapSolver, Anti-Captcha, CapMonster | https://api.ninetile.io/compat/capsolver (also /compat/anticaptcha, /compat/capmonster) |
| NopeCHA token API | https://api.ninetile.io/compat/nopecha |
# pip install 2captcha-python
from twocaptcha import TwoCaptcha
solver = TwoCaptcha("nt_live_YOUR_KEY", server="api.ninetile.io/compat/2captcha")
print(solver.hcaptcha(sitekey="…", url="https://example.com/login")["code"])Errors
Every non-2xx response has the same shape:
{ "error": { "code": "insufficient_credits", "message": "…", "param": null, "request_id": "9f1c2d3e-…" } }param names the offending field for validation errors. Retry-After is set when a response asks you to wait.
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_request | The body is not a JSON object, or not valid JSON. |
| 401 | unauthorized | The key is missing, malformed, unknown or revoked. |
| 402 | insufficient_credits | Your balance, after requests still running, does not cover the call. |
| 402 | key_credit_limit_exceeded | This key reached its own spend limit. |
| 403 | account_locked | The account is locked. Contact support. |
| 403 | forbidden | The key lacks the permission, or the request came from an address not on its allow-list. |
| 404 | not_found | No such solve or recognition on this account, or no such route. |
| 404 | not_eligible | Feedback for a solve that is unknown, not yours, or did not end solved. |
| 409 | conflict | The solve already finished, so it cannot be cancelled. |
| 413 | payload_too_large | The body is over 12 MiB, or an image is over 1.5 MiB. |
| 415 | unsupported_media_type | Send Content-Type: application/json. |
| 422 | validation_error | A field has a bad value; param names it. |
| 422 | expired_window | The first feedback for a solve arrived after 30 days. |
| 422 | recognition_failed | Puzzle not supported yet, or no answer found. Not charged. |
| 429 | concurrency_limit_exceeded | Too many requests running at once on this account. |
| 429 | rate_limited | Too many requests per minute, or too many failed sign-ins. |
| 429 | rate_capped | This account reached its submit rate for this sitekey. |
| 500 | internal_error | Our fault. Quote the request id. |
| 503 | maintenance | The solver is not ready. Retry shortly. |
| 503 | capacity_exhausted | At capacity, or the recognition service is unavailable. |
| 503 | proxy_pool_depleted | Solves without your own proxy are paused. Solves with your proxy still work. |
Limits
- Each account has a limit on requests running at once and on requests per minute. Both are shown in Settings; write to hello@ninetile.example for more.
- A sitekey is capped per account at 120 new solves a minute.
- Retry only failures marked retryable, and honour
Retry-After. - Tokens are not guaranteed to be accepted by every target. Report refusals through feedback.
This is v1. Fields are only ever added within a version, so ignore ones you do not recognise.