Ninetile

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 URLhttps://api.ninetile.io/v1
AuthAuthorization: Bearer nt_live_…
Content typeapplication/json
BillingPrepaid US dollars, charged only for work that succeeds

Quickstart

  1. Create an account and add balance in the dashboard.
  2. Create a key in API keys. It is shown once.
  3. Ask for a token. ?wait=60 holds 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_exceeded once it has spent its limit.

Responses carry X-Request-Id. Quote it when you contact support.

Pricing and billing

CallPrice
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 recognitionsFree

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

FieldDescription
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.
rqdataThe page's rqdata, when it provides one. Send it fresh for every challenge.
user_agentThe user agent of the browser that loaded the page.
proxyhttp://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] returns 200 once finished, 202 while running.
  • GET /v1/solves lists 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/feedback reports up to 500 verdicts at once: {"feedback":[{"solve_id","outcome","reason"?}]} with outcome accepted, rejected, unknown, unused or error. POST /v1/solves/{id}/feedback reports one.
  • GET /v1/me shows your balance and limits. GET /v1/usage?days=7 shows 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.codeRetryWhat happened
challenge_not_loadedyeshCaptcha did not serve a challenge. reason sitekey_rate_limited means it answered 429.
token_not_grantedyesThe challenge was answered but no token was issued.
challenge_expiredyeshCaptcha expired the challenge mid-solve.
proxy_erroryesYour proxy could not be used (or, without a proxy, our network could not reach hCaptcha).
vision_erroryesThe challenge was served but our answers were rejected.
unsolvable_variantyesA puzzle family we cannot solve yet. hCaptcha draws at random, so a retry usually works.
capacity_exhaustedyesBriefly at capacity. Retry in a few seconds.
internal_erroryesSomething failed on our side.
cancellednoYou cancelled it.
expiredyesThe 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

FieldDescription
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_dataBase64 images (bare or data:image/…;base64,). Grid: 1 to 27 tiles in order. Area select: exactly 1.
image_urlsThe same as URLs. Only https://*.hcaptcha.com addresses are fetched.
image_examples1 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: data is one boolean per tile, in the order you sent them. true means click it.
  • Area select: data is {"x","y","w":0,"h":0} in percent of the image: click at x, y. points lists every click.
  • Drag: data holds, per task, each piece's entity_id and box in percent. Drop its centre at x + 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 fromBase URL
2Captcha (in.php, res.php)https://api.ninetile.io/compat/2captcha
CapSolver, Anti-Captcha, CapMonsterhttps://api.ninetile.io/compat/capsolver (also /compat/anticaptcha, /compat/capmonster)
NopeCHA token APIhttps://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.

HTTPcodeMeaning
400invalid_requestThe body is not a JSON object, or not valid JSON.
401unauthorizedThe key is missing, malformed, unknown or revoked.
402insufficient_creditsYour balance, after requests still running, does not cover the call.
402key_credit_limit_exceededThis key reached its own spend limit.
403account_lockedThe account is locked. Contact support.
403forbiddenThe key lacks the permission, or the request came from an address not on its allow-list.
404not_foundNo such solve or recognition on this account, or no such route.
404not_eligibleFeedback for a solve that is unknown, not yours, or did not end solved.
409conflictThe solve already finished, so it cannot be cancelled.
413payload_too_largeThe body is over 12 MiB, or an image is over 1.5 MiB.
415unsupported_media_typeSend Content-Type: application/json.
422validation_errorA field has a bad value; param names it.
422expired_windowThe first feedback for a solve arrived after 30 days.
422recognition_failedPuzzle not supported yet, or no answer found. Not charged.
429concurrency_limit_exceededToo many requests running at once on this account.
429rate_limitedToo many requests per minute, or too many failed sign-ins.
429rate_cappedThis account reached its submit rate for this sitekey.
500internal_errorOur fault. Quote the request id.
503maintenanceThe solver is not ready. Retry shortly.
503capacity_exhaustedAt capacity, or the recognition service is unavailable.
503proxy_pool_depletedSolves 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.