Errors
Every status code returned by the API, the exact error strings, and what to do about each one.
Errors
All error responses share the same shape:
{ "success": false, "error": "<human-readable message>" }The HTTP status code is the primary signal — the message is for humans. Branch your retry logic on the status, not the string.
Reference table
| Status | When | Retry? | Action |
|---|---|---|---|
| 400 | Validation failed | No | Fix the offending field before retrying. |
| 401 | Missing or invalid API key | No | Verify X-API-Key matches an active key. |
| 402 | Insufficient balance | No | Top up. Reading /balance will show $0 or near it. |
| 403 | Type/IP/domain not allowed | No | Update key permissions, allowlist your IP, or stop targeting a banned domain. |
| 429 | CPM bucket empty | Yes | Wait, then retry. Capacity refills continuously. |
| 503 | Backend unavailable or failed | Yes | Retry with exponential backoff. Not billed. |
400 — Validation
The error string names the field that failed. The most common shapes:
{ "success": false, "error": "Invalid JSON" }
{ "success": false, "error": "Invalid type. Valid types: turnstile, challenge, kasada, akamai" }{ "success": false, "error": "site_key is required" }
{ "success": false, "error": "site_key contains invalid characters" }
{ "success": false, "error": "url is required" }
{ "success": false, "error": "url must use http or https" }
{ "success": false, "error": "url must not target internal addresses" }
{ "success": false, "error": "action contains invalid characters" }
{ "success": false, "error": "cdata contains invalid characters" }{ "success": false, "error": "proxy is required" }
{ "success": false, "error": "proxy format must be protocol://[user:pass@]host:port" }{ "success": false, "error": "kasada_config required for kasada type" }
{ "success": false, "error": "kasada_config.p_js_path contains invalid characters" }
{ "success": false, "error": "kasada_config.fp_host contains invalid characters" }
{ "success": false, "error": "kasada_config.tl_host contains invalid characters" }
{ "success": false, "error": "ua_version must be between 80 and 300" }{ "success": false, "error": "user_agent is required for akamai type" }
{ "success": false, "error": "proxy is required" }
{ "success": false, "error": "proxy format must be protocol://[user:pass@]host:port" }Length and character limits
| Field | Pattern / range | Max length |
|---|---|---|
url | http/https, public host | 2048 |
site_key | [a-zA-Z0-9_-] | 128 |
action, cdata | [a-zA-Z0-9._\-/: ] | 256 / 512 |
proxy | (http|https|socks4|socks5)://[user:pass@]host:port | 512 |
user_agent | No control characters | 512 |
kasada_config.p_js_path | Starts with /, no .. | 512 |
kasada_config.fp_host, tl_host | Hostname ([a-zA-Z0-9.-]), not an internal address | 256 |
kasada_config.cd_constant | Hex string | 128 |
| Request body total | — | 1 MB |
401 — Authentication
{ "success": false, "error": "Missing API key. Use X-API-Key header." }
{ "success": false, "error": "Invalid API key" }See Authentication for the header format.
402 — Insufficient balance
{ "success": false, "error": "Insufficient balance" }The pre-check failed because the key's balance is below the per-call cost. Top up; failed solves do not consume balance, so a 402 doesn't waste funds.
403 — Access denied
{ "success": false, "error": "IP not whitelisted" }
{ "success": false, "error": "Captcha type 'kasada' is not allowed for your account" }
{ "success": false, "error": "This domain is not allowed" }
{ "success": false, "error": "Access denied" }Access denied is returned when the originating IP is on the global ban list. Update the IP, request, or permission set as appropriate.
429 — Rate limited
{ "success": false, "error": "Rate limit exceeded: max 600 captchas per minute" }
{ "success": false, "error": "Rate limit exceeded" }The first form comes from the /solve CPM bucket. The second comes from the /balance per-IP limit (30/min). See Rate limits for sizing strategies.
503 — Backend errors
{ "success": false, "error": "No backend servers available for this captcha type" }
{ "success": false, "error": "Backend error" }
{ "success": false, "error": "Solve failed" }These are transient — a worker is unavailable or returned an unparseable response. Retry with exponential backoff. The CPM token is refunded automatically.
Retry policy in one sentence
Retry 429 and 503 with exponential backoff. Do not retry 4xx outside of 429 — the request is broken and won't succeed without changes.
Error handling skeleton
import time, requests
def solve(payload, attempts=4):
for i in range(attempts):
r = requests.post(
"https://api.nslsolver.com/solve",
headers={"X-API-Key": "YOUR_KEY"},
json=payload,
timeout=120,
)
data = r.json()
if r.status_code == 200:
return data
if r.status_code in (400, 401, 402, 403):
raise RuntimeError(f"non-retryable ({r.status_code}): {data['error']}")
if r.status_code in (429, 503):
time.sleep(min(2 ** i, 8))
continue
raise RuntimeError(f"unexpected ({r.status_code}): {data['error']}")
raise RuntimeError("attempts exhausted")