# 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://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://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://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. target is the protected URL or a bare host. provider is optional and defaults to kasada. 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 "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. Send the one your replay address sits in. Default Europe/London. ip The exit you will replay from (GET /api/ip through that proxy gives you the exact value). We place the payload's geography from it and check it against tz. Never logged. user_agent Accepted; the payload is built as one particular browser and answers as it 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 newest x-kpsdk-ct you hold, in full "st": 1786477464392, x-kpsdk-st from that response, in MILLISECONDS "fc": "…" } optional, x-kpsdk-fc from /mfc 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 (+ script, see DEFERRED SCRIPT below) 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. Plus script, as for interstitial. 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. DEFERRED SCRIPT (interstitial and slider): DataDome serves the challenge page in two forms and flips between them PER REQUEST. Inline: one self-contained ~560KB page. Deferred: an ~11KB shell containing