# Solvr > Anti-bot solving API covering kasada, datadome. Either we drive the whole chain through > your proxy and hand back a finished token bundle, or you drive it yourself and we > return the one request you cannot build. Charged only when a solve is returned; > failures, timeouts, rejected fingerprints and engine backpressure are never billed. Base URL: https://app.getsolvr.io/api Auth: `X-API-Key: solv_…` header, or `Authorization: Bearer solv_…` Providers live today: kasada, datadome Price: $2.00 per 1,000 solves ($0.002 each), prepaid, no subscription, $5 minimum top-up ## MCP A remote MCP server is available at https://app.getsolvr.io/mcp. Use it instead of raw HTTP if you speak the Model Context Protocol. It authenticates with the same API key. claude mcp add --transport http getsolvr https://app.getsolvr.io/mcp --header "X-API-Key: solv_…" Tools: submit_task, get_task, check_balance. There is no blocking solve tool, for the reason in "the one rule" above. ## The one rule Every endpoint's shape follows from who makes the network requests. we drive the network -> async POST /api/tasks + GET /api/tasks/{id}. We fetch the challenge, run it and post the result, all through YOUR proxy. That takes 5-6s and almost none of it is compute — it is somebody else's network. So it is submit-and-poll, and there is deliberately NO synchronous version: holding a connection across a stranger's proxy is how one slow exit stalls everything, and a result collectable by id cannot be lost with a dropped connection. you drive the network -> sync Everything else. You fetch the challenge from your own session and replay our answer from it; we only run the part you cannot. A few hundred ms of our CPU, no proxy bandwidth, answer in the first response. These take NO proxy field — there is no hop of ours to route — and hand back no task id to poll. Common to all of them: the answer is bound to the session that will use it. Tokens and cookies are IP-bound, single-use, and live for minutes. Send them from the same egress, on the same session, with the user_agent the response gave you. Never cache. ## Endpoints POST /api/tasks [kasada, full solve] The full-network Kasada solve: we fetch the challenge page and script through your proxy, run the script, post the result, and hand back a finished token bundle. Submit returns in ~1ms; nothing on our side holds a connection or a process while your solve runs, so concurrency is bounded by solving capacity rather than by held connections. There is no synchronous variant — see "the one rule" above. Body: { "target": "https://www.example.com/", "proxy": "http://user:pass@host:port", "provider": "kasada" } target and proxy are required; provider is optional (kasada only — DataDome has no full-network product, and naming it here returns 422). proxy is REQUIRED because the token is bound to the IP that earns it: run the solve through the same egress you will send the token from, with a fresh session per solve. Omitting it returns 422. "direct" is rejected. 202 -> { "task_id", "status": "queued" } Then poll GET /api/tasks/{task_id}. Results are delivered within about a second of the solve finishing. $0.002 each, on delivery only. GET /api/tasks/{task_id} Poll as fast as every 100ms. Free, never billed, and metered separately from submissions at 1,000/s per key, so polling can never consume the allowance you need to submit. Do not add backoff to chase a limit here. 202 -> { "task_id", "status": "processing" } still working, keep polling 200 -> { "task_id", "status": "success", "solution": {...}, "latency_ms", "timing": { "queued_ms", "solve_ms" } } latency_ms is end to end, submission to collection. timing splits it: queued_ms is us holding the job before starting it (capacity), solve_ms is the solve itself. Useful for telling a slow solve from a busy queue without guessing from your own wall clock. 200 -> { "task_id", "status": "failed", "kind", "error" } note: 200, not an HTTP error the poll succeeded; the solve is what failed. The sync routes below map kinds onto HTTP status instead, because they have nothing to poll. 200 solution -> { ct, st, cd, h, fc, r, version, user_agent, cookies } 404 -> { "error", "kind": "not_found" } POST /api/kasada/payload [kasada, solve-only] Payload transformer. You make every network request from your own proxy and session; we only run the protection script and return the request for you to POST. The token is then minted against your egress IP. ~150-250ms. $0.001 each. Synchronous. The transform is in the first response — there is no task to poll, no 202, no job id to come back for. The task_id is a receipt for your logs. NO proxy field, and none is needed: we make no network request on this endpoint. Body: { "script_content": "…the script you fetched…", "script_url": "https://host/…/ips.js?x-kpsdk-v=j-1.2.661", absolute, query intact "version": "j-1.2.661", from the challenge page URL "fp_html": "…", optional "user_agent": "…", optional "ip": "203.0.113.7", optional "accept_language": "en", optional "tz": "Europe/Lisbon" } optional Those three fields ARE the whole requirement; the last four are optional coherence hints for customers whose exits span geographies. Omit them all and the call is complete. tz IANA name, not an offset. The timezone the payload claims. Defaults to the engine's own, set to match our usual exit geography. Today this is the only one of the four that changes the payload. ip Reserved for deriving that timezone from the exit's geography. The lookup is not built yet, so it changes nothing today. Use tz. Never logged. user_agent Accepted; the payload is minted by one calibrated browser identity and that identity answers regardless. The response tells you which. accept_language Accepted; same story as user_agent. 200 -> { "task_id", "status": "success", "transform": { target, version, tl_url, headers: {...}, payload: "", user_agent, accept_language, timezone } } POST the base64-decoded payload to tl_url with those headers as application/octet-stream, on the same session that fetched the page and script. IMPORTANT: replay with the user_agent the response returned, not one of your own. The payload embeds it. Claiming a different browser on the wire fails a cross-check silently. IMPORTANT: this does NOT return the cd proof-of-work. It depends on the collector's own response, which only your session sees. Mint one per protected request with POST /api/kasada/cd (free, below). Body limit 8MB, measured AFTER decompression (the script is ~0.5MB). Send Content-Encoding: gzip if you can; a ~0.5MB script travels as ~150KB. deflate also works. br and zstd do not and return 415. Responses are compressed if you send Accept-Encoding. 400 bad_request here means a stale/truncated script or a relative script_url, not a bad key. Same concurrency pool as solves. A slot is held only for the duration of the call, so a ~200ms transform frees it ~25x faster than a ~5s solve: the same tier goes much further. POST /api/kasada/cd [kasada, free] FREE — never decrements your balance, never appears on an invoice. Requires a key. Mints a fresh x-kpsdk-cd proof-of-work for a token you already hold. Kasada demands a NEW proof on every request to a protected endpoint; a replayed one goes stale and starts failing. So one call per protected request is the intended pattern, and pricing it would put a price on doing this correctly. Body: { "target": "https://www.example.com/", "ct": "…", the FULL cleared x-kpsdk-ct (min 32 chars; a real one is ~175) "st": 1786477464392, x-kpsdk-st from that response, in MILLISECONDS "fc": "…" } optional, x-kpsdk-fc from /mfc — difficulty params 200 -> { "status": "success", "target", "cd": { workTime, id, answers, duration, d, st, rst } } Send the cd object JSON-encoded as the x-kpsdk-cd header, next to your current x-kpsdk-ct. Two things that will trip you up: - st is MILLISECONDS here. A solve response returns st in SECONDS. Sending seconds is a valid integer, so only a range check catches it: we return 422 rather than a proof computed against 1970. - Kasada rotates x-kpsdk-ct on nearly every response. Derive each proof from the NEWEST ct you hold, not the one you started the session with. Proofs are computed from what is already known about a host, so a host that has never been solved returns 400. One full solve through /api/tasks against it is enough, once, for anyone. Rate limited per key at 200/s — free is a billing statement, not an access one. POST /api/datadome/solve [datadome, solve-only] DataDome throws three different things at a scraper and this one endpoint solves all three, switched on `type`. You fetch whatever it threw, send it here, replay our answer from your own session — that is what mints the datadome cookie against YOUR exit. No proxy field, no task to poll. "type": "interstitial" | "slider" | "tags" interstitial the "checking your device" page. ~350ms. $0.003 each. send: device_link (query string intact) + html get: { payload, post_url, headers, user_agent } replay: POST payload to post_url with those headers, as application/x-www-form-urlencoded, on the session that fetched the page. A pass answers {"view":"redirect","cookie":"datadome=…"}. If it answers "view":"captcha" your session escalated — fetch that page and come back with type "slider". slider the image puzzle. We locate the notch and drive a paced, human-shaped drag; the pacing IS the product, which is why it is not instant. 2-4s. $0.003 each. send: captcha_link + html, plus puzzle and piece (base64 images) if the puzzle variant was drawn. Omit both for the "simple" slider — a full-right drag with no notch. That is a real variant, not a missing field. get: { check_url, headers, user_agent } replay: GET check_url with those headers on the same session. tags the passive telemetry the DataDome script posts. Not a challenge: it raises session trust BEFORE one is drawn, so sending these means drawing fewer of the two above. ~200ms. $0.003 each. send: tags_js + page_url + ddk. Optional: tags_type ("ch", default, the telemetry sent on load | "le", the behavioural burst on pagehide — pass your current datadome cookie as cid with "le", that POST carries the rotated cookie), cid, version (e.g. "5.9.2"), endpoint/tags_url overrides, referrer. get: { endpoint, content_type, body } replay: POST body to endpoint as content_type from the tagged page's session. Nothing you need comes back; the trust is the point. 200 -> { "task_id", "status": "success", "type", "solution": { … as above … } } IMPORTANT: html is RAW (a JSON string, exactly as you fetched it), while puzzle and piece are base64. Sending base64 html is the most common 422 here. IMPORTANT: replay with the user_agent the response returned, not one of your own, and send the returned headers verbatim. The payload embeds that browser identity; claiming another one fails a platform cross-check silently, and it looks just like a bad solve. Optional on all three, same names and meanings as /api/kasada/payload: tz IANA name, not an offset. The timezone the payload claims, checked against the geo of the IP that replays it. Default Europe/London. The only one of the four that changes the payload today. ip the exit you will replay from. Reserved for deriving that timezone; the lookup is not built yet, so it changes nothing. Use tz. Never logged. user_agent accepted; one calibrated identity answers regardless, and the response tells you which. accept_language used where the real header set carries one. Default en-GB. URL allowlists, and they are NOT interchangeable: device_link and captcha_link must be on *.captcha-delivery.com; endpoint and tags_url must be on *.datadome.co. 2MB per field after decompression. gzip and deflate accepted; br and zstd return 415. 400 bad_request = a stale page, a link missing its query string, or an image that did not decode. 502 upstream = the page ran but produced no solve request, which normally means the challenge no longer matches the session it came from: fresh session, fresh challenge. 503 busy = our queue, honour Retry-After. None of these are billed. Not our fault and not yours: which variant DataDome draws is sticky to the exit IP and reflects its reputation — roll a fresh sticky session and retry. On the strictest sites your REPLAY cadence is graded too, so mirror human timing between page load and form POST. GET /api/balance 200 -> { "balance", "balance_micros", "currency", "providers": { "": { "kinds": { "": { price, base_price, remaining, free, promotion } } } } } Priced per vendor AND per kind, because the two vendors sell different things: kasada has kinds solve/payload/cd, datadome has interstitial/slider/tags. price_per_solve is present only on a vendor that actually sells a full solve — absent, never synthesised, on one that does not. balance_micros is authoritative; balance is a rendering of it. Money is integer micro-dollars: 1 USD = 1,000,000 micros, a full solve is 2,000. ## Using what you get back Everything this API returns is bound to the session that will use it — a Kasada bundle, a DataDome cookie, a replay instruction. All of them are IP-bound, single-use, and live for minutes. Send from the same egress IP, on the same session, reusing the user_agent from the response verbatim, and set any cookies the response carried. Do not cache any of it. We strip stored results within 72 hours. ## Errors Every failure carries a machine-readable `kind`. None of them are billed. bad_request retryable=no The request was invalid. Check the target and proxy fields. unsupported_target retryable=no That target is not supported yet. Report the host and it will be tuned. unauthorized retryable=no The solver is misconfigured on our side. This has been reported; please retry shortly or contact support. busy retryable=yes (backpressure, retried for you, never surfaced on the async path) The solver is at capacity. Retry shortly. bad_fingerprint retryable=yes The session was graded as a bad fingerprint (burned IP). Retry with a fresh proxy session. rejected retryable=yes The target refused the solve. If this persists on this host, report it so the profile can be tuned. upstream retryable=yes The target site was unreachable, or the proxy failed. Check your proxy and retry with a fresh session. upstream_timeout retryable=yes The solve exceeded the deadline. This is almost always a dead or slow proxy. Retry with a fresh proxy session. bad_proxy retryable=no Your proxy could not be used. It refused the connection, could not be resolved, or rejected the credentials in the proxy string. Check the proxy and retry with a working one — this was not retried for you, because a retry would use the same proxy. version_extraction retryable=no The solver could not read this target's current protection script. This is a fault on our side and has been reported; it needs a fix here rather than a change to your request. transport retryable=yes The solver is temporarily unreachable. Retry shortly. bad_fingerprint, upstream and upstream_timeout are nearly always a dead or slow proxy. Retry with a fresh proxy session rather than treating it as an outage. On the SYNCHRONOUS routes (/api/kasada/payload, /api/datadome/solve, /api/kasada/cd), kinds map onto HTTP status. The async path reports failure as 200 + status "failed": 400 bad_request, unsupported_target 402 insufficient_balance 429 concurrency_limit_reached 500 server_error (unhandled fault on our side; never billed, safe to retry) 502 unauthorized, upstream, rejected, bad_fingerprint, transport 503 busy (retry shortly) 504 upstream_timeout ## Limits Throughput is governed by concurrency, meaning how many solves are in flight at once, not by a requests-per-minute quota. Starter 5 concurrent (any spend) Growth 10 concurrent ($50+ in 30 rolling days) Scale 25 concurrent ($250+ in 30 rolling days) Pro 50 concurrent ($1,000+ in 30 rolling days) Accounts that have never spent get 1 concurrent. Exceeding it returns 429 with kind concurrency_limit_reached. Underneath that there are flood backstops. They are per API key, never per IP, and measured per second, so an overshoot costs a second rather than the remainder of a minute — and traffic from anyone sharing your egress is never charged against you. submissions 100/s POST /api/tasks, /api/kasada/payload, /api/datadome/solve polling 1,000/s GET /api/tasks/{task_id} — supports a 100ms interval at any tier proof of work 200/s POST /api/kasada/cd Exceeding one returns 429 with Retry-After. These are backstops against runaway clients, not throughput shaping: concurrency is the limit that governs your rate. ## Webhooks Register an https endpoint in the dashboard to receive signed JSON instead of polling. Events: task.completed, task.failed, balance.low, balance.exhausted, key.revoked, payment.completed. Signature: `Solvr-Signature: t=,v1=` where hex is hmac_sha256("{t}.{raw_body}", secret). The timestamp is inside the signed string, so captured deliveries cannot be replayed. Respond 2xx within 15s; retried at 10s, 1m, 5m, 30m, 2h. ## Full documentation https://app.getsolvr.io/docs