API reference
Four endpoints, one header.
Everything is JSON over HTTPS. There is no SDK and no handshake: send your key on the request and POST a target. This page is generated from the running application, so the prices, limits and error kinds below are the ones actually in force.
Contents
Getting started
Everything else
Getting started
Authentication
Send your key as X-API-Key, or as a
standard bearer token. Keys start solv_
and are shown once at creation, and we store only a hash, so a lost key must be
replaced rather than recovered.
X-API-Key: solv_live_9f2c…
or
Authorization: Bearer solv_live_9f2c…
Auth failures
| Status | Kind | Cause |
|---|---|---|
| 401 | missing_key | No key on the request. |
| 401 | invalid_key | Unknown or revoked key. |
| 402 | insufficient_balance | Balance exhausted. Top up to receive a new key. |
note: keys are revoked automatically the moment a balance reaches zero. revocation is final: top up, then create a new key.
How this API is shaped
Two vendors, 2 of them live today, and one rule that decides the shape of every endpoint below. It is worth thirty seconds, because once you have it you can predict what each route does without reading its section.
we drive the network → async
One endpoint works this way: /api/tasks. We fetch the challenge, run it and post the result — all of it through your proxy. That takes five to six seconds, and almost none of it is compute; it is somebody else's network.
So it is submit-and-poll, and there is no synchronous version to reach for by mistake. Holding a connection open across a stranger's proxy is how one slow exit takes a server worker out of circulation, and the result being collectable by id is what stops a dropped connection costing you a solve you paid for.
you drive the network → sync
Everything else: the transformer, all three DataDome types, and proof-of-work. You fetch the challenge from your own session and replay our answer from it. We only run the part you cannot.
That is a few hundred milliseconds of our CPU and no proxy bandwidth at all, so the answer is in the first response. None of these take a proxy field — there is no hop of ours to route — and no task id to come back for.
| Endpoint | Vendor | Who fetches | Shape | Typical | Price |
|---|---|---|---|---|---|
| /api/tasks | Kasada | us | submit + poll | 5-6s | $0.002 |
| /api/kasada/payload | Kasada | you | sync | 150-250ms | $0.001 |
| /api/kasada/cd | Kasada | nobody | sync | <1ms | free |
| /api/datadome/solve · interstitial | DataDome | you | sync | ~350ms | $0.003 |
| /api/datadome/solve · slider | DataDome | you | sync | 2-4s | $0.003 |
| /api/datadome/solve · tags | DataDome | you | sync | ~200ms | $0.003 |
One thing is common to all of them, and it is the thing most likely to cost you a solve: every answer is bound to the session that will use it. Tokens and cookies are minted against an IP, they are single-use, and they live for minutes. Send them from the same egress, on the same session, with the user agent the response gave you — and never cache one.
Kasada · full solve
/api/tasks
· GET /api/tasks/{task_id}The full-network Kasada solve: we fetch the challenge page and script through your proxy, run the protection script, post the result and hand back a finished token bundle. Nothing about the chain is yours to build.
Submit returns in about a millisecond with a task id. Collect the result by polling, or register a webhook and skip polling entirely. There is no synchronous version — see how this API is shaped for why, and the transformer if you would rather own the network yourself and get an answer in one call.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| target | string | yes | The protected URL, a bare host, or a tuned profile name. Aliases resolve to the same profile, so twitch, www.twitch.tv, gql.twitch.tv and https://gql.twitch.tv/gql all work — the solve runs against the host you passed, and the response's target tells you which. Max 2048 characters. Unknown hosts get the generic profile. |
| provider | string | no | One of kasada, datadome. Defaults to kasada. |
| proxy | string | yes | The proxy this solve runs through, e.g. http://user:pass@host:port. The token is bound to the IP that earns it, so this has to be the egress you will send the token from — a solve on somebody else's address succeeds and then fails downstream. Use a fresh session per solve. Never written to disk: only scheme, host and port survive in debug records. |
Poll as fast as every 100ms. That is a floor we support rather than a limit we tolerate: this endpoint reads one row, is never billed, and exists so you can collect a result the moment it lands. At 1,000 polls per second per key, that floor covers 100 tasks in flight at once — well past the highest concurrency tier. Polling is metered separately from submissions, so checking on work in progress can never consume the allowance you need to submit more of it.
$ curl -X POST https://app.getsolvr.io/api/tasks \
-H "X-API-Key: solv_live_9f2c…" \
-d '{"target":"https://www.example.com/"}'
{
"task_id": "3f6a1c02-7b19-4f5e-9a2d-0c8e1b4d5a77",
"status": "queued"
}
202 · still working
{ "task_id": "3f6a…", "status": "processing" }
200 · solved
{ "task_id": "3f6a…", "status": "success",
"solution": { … }, "latency_ms": 5240 }
200 · failed
{ "task_id": "3f6a…", "status": "failed",
"kind": "bad_fingerprint", "error": "…" }
A task that is still queued or running returns 202
with status processing, including while it
is being retried internally after backpressure. You never see a transient
error, and a failed task returns 200 with a
typed kind, not an HTTP error.
An unknown id returns 404 with kind
not_found.
Solution fields
| Field | Meaning |
|---|---|
| ct | The token payload. This is what you send to the protected site. |
| st | Server time the bundle was minted, as a unix timestamp. |
| cd | Client data accompanying the token. |
| h / fc | Auxiliary handshake values, present when the profile emits them. |
| r | Grading verdict. Anything starting 1-B is a pass. Exposed so you can correlate against your own accept rate. |
| version | The script generation the bundle was solved against. |
| user_agent | The user agent the bundle was minted with. Send the same one. |
| cookies | Cookies to set alongside the token. |
bundles are ip-bound, single-use and live for minutes. we strip them within 72h.
Kasada · payload transformer
/api/kasada/payload
availableThe opposite division of labour to /api/tasks. You make every network request, from your own proxy, TLS session and cookie jar. We do the one part you can't: run the protection script. You get back the exact request to POST, so the token is minted against your egress IP rather than ours.
That matters because tokens are IP-bound. On the full solve you have to keep our session and yours aligned; here there is only ever one session, and it is yours. A transform is roughly 150-250ms and moves no proxy bandwidth through us, which is why it costs $0.001 against $0.002 for a full solve. It is the right endpoint if you already run a browser-grade HTTP stack — and the wrong one if you don't, because everything either side of the transform is yours to build.
read this before integrating
This endpoint does not return the
cd proof-of-work, and the
protected site wants a fresh one on every request. The proof depends on the
collector's own response and on a config only your session sees, so it cannot
be computed here — only by whoever made that request.
So mint each one as you go with
/api/kasada/cd,
which takes the ct and
st your collector response
gave you and hands back a proof.
It is free and does not draw your balance down —
one call per protected request is the intended pattern, so charging for it
would put a price on doing this correctly.
three fields is a complete call
script_content,
script_url and
version are the whole
requirement. There is no proxy field — we make no network request, so there is
nothing to route. The four fields below them
(user_agent,
ip,
accept_language,
tz) are for keeping the
payload's own idea of where it is running aligned with the address you replay
it from. If your exits sit in one place, omit them.
What you get back is not an echo of what you sent. The payload is minted by a
single calibrated browser identity, so the response carries the
user_agent,
accept_language and
timezone it was actually
built with. Replay with those. A payload that
claims one browser while your request headers claim another fails a
cross-check you never see.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| script_content | string | yes | The protection script source, exactly as you fetched it. Around 0.5MB. Send it byte for byte: a re-encoded or truncated script produces no token and returns 400. |
| script_url | string | yes | The absolute URL you fetched that script from, query string intact. The path and query carry session parameters and the host selects the profile. A relative path copied out of the page HTML is the most common cause of a 400 here. |
| version | string | yes | The x-kpsdk-v value from the challenge page URL. A stale version against a fresh script still runs, because the script's own captured version wins. |
| fp_html | string | no | The challenge page HTML, if you kept it. |
| provider | string | no | One of kasada, datadome. Defaults to kasada. |
| user_agent | string | no | The user agent your replay requests will send. Accepted, but see the note below: the payload is minted by one calibrated browser identity, and the response tells you which. There is no proxy field here and there is no need for one. |
| ip | string | no | The exit address you will replay from. Reserved for deriving the payload timezone from its geography; that lookup is not built yet, so today it changes nothing. Use tz until it is. Never logged. |
| accept_language | string | no | The first language of your Accept-Language header. Accepted; the persona answers with its own, which comes back on the response. |
| tz | string | no | IANA name, e.g. Europe/Lisbon — not an offset. The timezone the payload claims. Defaults to whatever the engine is running as, which is set to match our usual exit geography. This is the one of the four that changes the payload today. |
body limit 8MB, measured after decompression. this is the one
endpoint here with a large body, so check your own client's limits too.
send Content-Encoding: gzip if your stack
can — script source compresses about five to one and at this size it is worth it.
deflate works too; br and zstd do not, and are
refused with a 415 rather than silently mangled.
responses are compressed whenever you send
Accept-Encoding, in whatever you ask for.
$ curl -X POST https://app.getsolvr.io/api/kasada/payload \
-H "X-API-Key: solv_live_9f2c…" \
-H "Content-Type: application/json" \
-d '{
"script_content": "…the ips.js you fetched…",
"script_url": "https://www.example.com/149e/2d20/ips.js?x-kpsdk-v=j-1.2.661",
"version": "j-1.2.661"
}'
{
"task_id": "9c1e4a7b-2f60-4d13-8ba5-7e0d2c9f1a34",
"status": "success",
"transform": {
"target": "www.example.com",
"version": "j-1.2.661",
"tl_url": "https://www.example.com/149e/2d20/tl",
"headers": {
"x-kpsdk-ct": "…",
"x-kpsdk-dt": "…",
"x-kpsdk-im": "…",
"x-kpsdk-v": "j-1.2.661"
},
"payload": "<base64>",
"user_agent": "Mozilla/5.0 …",
"accept_language": "en",
"timezone": "America/New_York"
}
}
What you do around us
| Step | Who | What happens |
|---|---|---|
| 1 | you | GET the protected page through your proxy. Keep the challenge script URL and the x-kpsdk-v from it. |
| 2 | you | GET that script URL on the same session. Keep both the URL and the body. |
| 3 | us | POST them here. We run the script and return the collector request. |
| 4 | you | POST the base64-decoded payload to tl_url with the returned headers, as application/octet-stream, plus your normal XHR headers (origin, referer, sec-fetch-*), on that same session — sending the user_agent the response gave you. The response carries your token. |
| 5 | you | GET /mfc with x-kpsdk-h: 01 and x-kpsdk-v for the remaining headers, then compute a fresh cd for each protected request. |
One session throughout. The session that fetched the page and script must be the session that POSTs the payload and then calls the protected endpoint — same IP, same cookie jar, same TLS fingerprint. Swap any of them and the token is being presented from an address it was never minted for.
Failures here
A narrower set than a solve's, because the network is yours: nothing here can fail on a burned proxy IP or an unreachable target. Those failures still exist — they are just yours to see and handle now.
| Status | Kind | What to do |
|---|---|---|
| 400 | bad_request | Your input. Usually a stale or truncated script, or a relative script_url. Re-fetch the page and script and send the fresh pair. Retrying the same body cannot succeed, so do not burn a retry loop on it. |
| 400 | unsupported_target | That host has no profile yet. Report it; do not retry. |
| 402 | insufficient_balance | Top up. A new key is issued on top-up. |
| 503 | busy | Our engine queue is full. Honour Retry-After (about a second) and send it again. Never billed. |
billed $0.001 per delivered transform, on delivery only, same as a solve.
same concurrency allowance as solves, drawn from one pool. a slot is held
only while the call runs, so at ~200ms a transform returns it roughly 25x faster
than a solve does — the same tier simply goes much further here.
Kasada · proof-of-work (free)
/api/kasada/cd
free
Mints a fresh x-kpsdk-cd
proof-of-work for a token you already hold. Kasada wants a new one on
every request to a protected endpoint — replay one
across requests and it goes stale and starts failing — so scraping ten thousand
pages means ten thousand calls here.
Which is exactly why it costs nothing. Billing per proof would put a price on doing this properly and quietly reward the customer who replays a stale one until it breaks. It is a few hundred hashes with no network behind them, so there is nothing here worth charging you for. It does not decrement your balance and it does not appear on your invoice.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| target | string | yes | The protected URL or host, same as a solve. |
| ct | string | yes | The full cleared x-kpsdk-ct from your collector response. Minimum 32 characters — a real one is around 175, so anything shorter is a truncated or placeholder token and is refused before it reaches the engine. Kasada rotates this on nearly every response: always use the newest one you hold and derive the proof from that, not from the token you started with. |
| st | integer | yes | The x-kpsdk-st from that same response, in MILLISECONDS. Note a solve response returns st in seconds; this endpoint wants milliseconds, and sending seconds is the one mistake everybody makes exactly once. |
| fc | string | no | The x-kpsdk-fc from the /mfc handshake. Supplies the difficulty parameters; omit it and pinned per-target values are used. |
$ curl -X POST https://app.getsolvr.io/api/kasada/cd \
-H "X-API-Key: solv_live_9f2c…" \
-H "Content-Type: application/json" \
-d '{
"target": "https://www.example.com/",
"ct": "0KZ2Vr83HIoF0KYB2i3gZNiU39X6…",
"st": 1786477464392
}'
{
"status": "success",
"target": "www.example.com",
"cd": {
"workTime": 1786477507766,
"id": "c0ce36a178fc32…",
"answers": [1, 7],
"duration": 18.2,
"d": 89,
"st": 1786477464392,
"rst": 1786477464481
}
}
Send the cd object
JSON-encoded as the x-kpsdk-cd
header, next to your current
x-kpsdk-ct.
one limitation
A proof is computed entirely from what is already known about a host — the call
touches no network at all, which is what makes it instant and free. So a host
that has never been solved returns
400: there is nothing on
record to compute against yet.
One full solve against that host fixes it permanently, and for everyone rather than just your account — so in practice you will only meet this on a host nobody has solved yet.
free does not mean unauthenticated: the call still needs your key, and is rate limited per key at 200/s — roughly 2x the ceiling on submissions, because one proof per protected request is the intended pattern.
DataDome · all three challenges
/api/datadome/solve
available
DataDome throws three different things at a scraper, and this one endpoint
solves all three. You fetch whatever it threw, send it here, and get back the
exact request to replay from your own session — which is what mints the
datadome cookie against
your egress rather than ours.
One route rather than three, switched on type,
because you do not know which challenge you are about to meet until you meet it.
DataDome picks; the branch belongs on a field, not on a URL. There is no proxy
field and no task to poll —
see how this API is shaped.
The three types
| type | What it is | You send | You replay | Typical | Price |
|---|---|---|---|---|---|
| interstitial | The "checking your device" page. The most common escalation, and the one you will integrate first. | device_link + html | POST payload to post_url | ~350ms | $0.003 |
| slider | The image puzzle. We locate the notch in the background and drive a paced, human-shaped drag — the pacing is the product, which is why it is not instant. | captcha_link + html (+ puzzle, piece) | GET check_url | 2-4s | $0.003 |
| tags | The passive telemetry the DataDome script posts. Not a challenge at all: it raises session trust BEFORE a challenge is drawn, so sending these means drawing fewer of the two above. | tags_js + page_url + ddk | POST body to endpoint | ~200ms | $0.003 |
read this before integrating
html is raw; the images are base64. That asymmetry is deliberate — the page body travels as a JSON string, which is one encoding step fewer for you to get wrong, while a JPEG cannot. Sending base64 HTML is the most common 422 here.
Replay with the user agent we return, not your
own. Every response carries
user_agent — the browser
identity the payload was actually built as — along with the exact
headers to send. Claiming
a different browser on the wire fails a platform cross-check you never see,
and it looks exactly like a solve that simply did not work.
Body
| Field | Type | Required for | Notes |
|---|---|---|---|
| type | string | all | One of interstitial, slider, tags. Everything else on this table follows from it. |
| device_link | string | interstitial | The device-check URL you were redirected to, query string intact — it carries the session id and the referer we need. Must be on *.captcha-delivery.com. |
| captcha_link | string | slider | The captcha URL, query string intact. Must be on *.captcha-delivery.com. |
| html | string | interstitial, slider | The challenge page body exactly as you fetched it. RAW, not base64. A device-check page runs around half a megabyte. |
| puzzle | string | — | Base64 of the background .jpg. Omit it and you get the "simple" slider, which is a full-right drag with no notch to find — that is a real variant, not a missing field. |
| piece | string | — | Base64 of the .frag.png piece. Accepted; the notch is located from the background alone today. |
| tags_js | string | tags | The tags.js source as you fetched it. |
| page_url | string | tags | The page the script was running on. |
| ddk | string | tags | The site key from the page's DataDome config. |
| tags_type | string | — | "ch" (default — the telemetry sent on page load) or "le" (the behavioural burst sent on pagehide). For "le", pass the datadome cookie you currently hold as cid: that POST carries the rotated cookie. |
| cid | string | — | Your current datadome cookie value, if the session has one. |
| version | string | — | The tags.js version, e.g. 5.9.2. The page exposes it as window.dataDomeOptions.version. |
| endpoint / tags_url | string | — | Overrides for sites that configure their own. Must be on *.datadome.co — note that is a different domain from the challenge links above, and they are not interchangeable. |
| user_agent | string | — | Accepted. The solve runs one calibrated browser identity and answers with it regardless; the response tells you which. See the note above. |
| ip | string | — | The exit address you will replay from. Reserved for deriving the payload timezone from its geography; that lookup is not built yet, so today it changes nothing. Use tz until it is. Never logged. |
| accept_language | string | — | The first language of your Accept-Language header. Used where the real header set carries one; default en-GB. |
| tz | string | — | IANA name, e.g. Europe/Lisbon — not an offset. The timezone the payload claims, cross-checked against the geography of the IP that replays it. Default Europe/London. This is the one of the four that changes the payload today. |
2MB per field, measured after decompression.
send Content-Encoding: gzip if your stack
can — a captured page compresses about five to one.
deflate works too; br and zstd do not, and are
refused with a 415 rather than silently mangled.
$ curl -X POST https://app.getsolvr.io/api/datadome/solve \
-H "X-API-Key: solv_live_9f2c…" \
-H "Content-Type: application/json" \
-d '{
"type": "interstitial",
"device_link": "https://geo.captcha-delivery.com/interstitial/?initialCid=…",
"html": "…the page you just fetched…"
}'
{
"task_id": "9c1e4a7b-2f60-4d13-8ba5-7e0d2c9f1a34",
"status": "success",
"type": "interstitial",
"solution": {
"payload": "…url-encoded form body…",
"post_url": "https://geo.captcha-delivery.com/interstitial/",
"headers": { … send all of these … },
"user_agent": "Mozilla/5.0 …"
}
}
What comes back
Three shapes, because the replays are three different actions. Each one carries only the fields its own type needs, so there is nothing to test for null.
| type | solution fields | What you do |
|---|---|---|
| interstitial | payload, post_url, headers, user_agent | 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" instead, your session escalated — fetch that captcha page and come back with type slider. |
| slider | check_url, headers, user_agent | GET check_url with those headers on the same session. The response carries the datadome cookie. |
| tags | endpoint, content_type, body | POST body to endpoint as content_type, from the tagged page's session. Nothing comes back that you need — the point is the trust it builds. |
Things that are not our fault, and not yours either
| What you see | What it means |
|---|---|
| A slider where you expected a device check | Which variant DataDome draws is sticky to the exit IP and reflects its reputation, not your solve. Roll a fresh sticky session and retry — the same rule as a burned proxy on Kasada. |
| A refused cookie on a strict site | On the strictest integrations your REPLAY cadence is graded too. Going from page load to form POST with zero dwell pre-grades the session even with a perfect payload. Mirror human timing on form flows; on ordinary sites it does not matter. |
| It worked once and not again | Challenge pages are single-use. A page fetched for one attempt cannot be re-sent for another — fetch a fresh one each time. |
Failures here
| Status | Kind | What to do |
|---|---|---|
| 400 | bad_request | Your input. A stale challenge page, a link missing its query string, or a puzzle image that did not decode. Re-fetch and send the fresh set. Retrying the same body cannot succeed. |
| 402 | insufficient_balance | Top up. A new key is issued on top-up. |
| 422 | — | Validation, before anything reached the engine. The response names the field. |
| 502 | upstream | The page ran but produced no solve request — normally a challenge page that no longer matches the session it came from. Fresh session, fresh challenge, retry. |
| 503 | busy | Our engine queue is full. Honour Retry-After (about a second). Never billed. |
Billing is the same rule as everywhere else on this API: you pay for a returned solve and nothing else. A 4xx, a 5xx and backpressure all cost nothing.
Everything else
/api/balance
Current balance and the live per-provider price, including any promotion you qualify for. Free to call and never billed.
{
"balance": "42.5000",
"balance_micros": 42500000,
"currency": "USD",
"providers": {
"kasada": {
"price_per_solve": "0.002000",
"base_price_per_solve": "0.002000",
"solves_remaining": 21250,
"free": false
}
}
}
money is integer micro-dollars: 1 USD = 1,000,000 micros. a solve is
2,000 of them. balance_micros
is the authoritative figure; balance is a rendering of it.
when a promotion applies, each provider also carries a
promotion object with name, discount_percent and ends_at.
Errors
Every failure carries a machine-readable kind
alongside a human message, so you can branch on the cause instead of parsing
prose. None of these are billed.
| Kind | Retry? | What it means |
|---|---|---|
| bad_request | no | The request was invalid. Check the target and proxy fields. |
| unsupported_target | no | That target is not supported yet. Report the host and it will be tuned. |
| unauthorized | no | The solver is misconfigured on our side. This has been reported; please retry shortly or contact support. |
| busy | yes | The solver is at capacity. Retry shortly. backpressure, retried for you, never surfaced on the async path, never billed |
| bad_fingerprint | yes | The session was graded as a bad fingerprint (burned IP). Retry with a fresh proxy session. |
| rejected | yes | The target refused the solve. If this persists on this host, report it so the profile can be tuned. |
| upstream | yes | The target site was unreachable, or the proxy failed. Check your proxy and retry with a fresh session. |
| upstream_timeout | yes | The solve exceeded the deadline. This is almost always a dead or slow proxy. Retry with a fresh proxy session. |
| bad_proxy | 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 | 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 | yes | The solver is temporarily unreachable. Retry shortly. |
HTTP status on the synchronous routes
The two are read differently, and the difference is worth knowing before you
write the error handling. A collected task reports its failure as
200 with a failed status —
the poll succeeded; the solve is what did not. The synchronous routes have
nothing to poll, so they map the kind onto the status directly:
| Status | Kinds |
|---|---|
| 400 | bad_request, unsupported_target |
| 429 | concurrency_limit_reached, you are at your tier ceiling |
| 402 | insufficient_balance |
| 500 | server_error: an unhandled fault on our side. Never billed. Safe to retry. |
| 502 | unauthorized, upstream, rejected, bad_fingerprint, transport |
| 503 | busy: retry shortly |
| 504 | upstream_timeout: almost always a slow or dead proxy |
Webhooks
Register an https endpoint in your dashboard and we POST signed JSON as things happen. Solve events fire for async tasks only, since a synchronous call already returned you the answer.
| Event | Fires when |
|---|---|
| task.completed | An async solve finished successfully. |
| task.failed | An async solve failed after its final retry. |
| balance.low | Balance reached your alert threshold. |
| balance.exhausted | Balance hit zero. Solving stops and keys are revoked. |
| key.revoked | An API key was revoked, for any reason. |
| payment.completed | A top-up was credited. |
Verifying a delivery
The timestamp is inside the signed string, so a captured request cannot be replayed later with a fresh one. Compare in constant time and reject anything older than about five minutes.
Solvr-Event: task.completed
Solvr-Delivery: 8c1d4e77-2a90-4b13-bf65-…
Solvr-Signature: t=1699999999,v1=<hex>
Content-Type: application/json
import hmac, hashlib, time
t, v1 = parse(headers["Solvr-Signature"])
if abs(time.time() - int(t)) > 300:
raise Reject("stale")
expected = hmac.new(
secret.encode(),
f"{t}.{raw_body}".encode(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, v1):
raise Reject("bad signature")
Respond 2xx within 15 seconds. Anything else
is retried at 10s, 1m, 5m, 30m and 2h. After
15 consecutive failures the endpoint is
suspended until you re-enable it. Use
Solvr-Delivery to deduplicate: a retry
reuses the same id.
MCP / AI agents
liveWe run a remote Model Context Protocol server, so an agent that hits a Kasada wall mid-task can solve it and carry on instead of failing. Works with any MCP client: Claude Code, Claude Desktop, Cursor, Codex, Copilot.
There is no separate credential: it authenticates with the same API key, and the same concurrency, balance and revocation rules apply.
Connect
$ claude mcp add --transport http getsolvr \
https://app.getsolvr.io/mcp \
--header "X-API-Key: solv_live_9f2c…"
{
"mcpServers": {
"getsolvr": {
"type": "http",
"url": "https://app.getsolvr.io/mcp",
"headers": {
"X-API-Key": "solv_live_9f2c…"
}
}
}
}
Tools
| Tool | What it does |
|---|---|
| submit_task | Queue a Kasada solve and return a task id immediately. |
| get_task | Collect a queued solve, or see that it is still processing. |
| check_balance | Prepaid balance and the live price of every kind of call. Free. |
The server ships instructions telling the agent when to reach for a solve, that failures are free to retry, and that bundles are IP-bound and single use, so it generally does the right thing without being told.
there is also a plain-text summary of this page at /llms.txt for agents that just want to read the docs.
Limits and billing
Throughput is governed by concurrency (how many solves you have in flight at once), not by a requests-per-minute quota. A solve takes seconds, so requests per minute is the wrong unit.
| Tier | Spend, rolling 30 days | In flight |
|---|---|---|
| Evaluating | $0 spent | 1 |
| Starter | any spend | 5 |
| Growth | $50+ | 10 |
| Scale | $250+ | 25 |
| Pro | $1,000+ | 50 |
Tiers are matched on top-up spend across a rolling 30 days, and rolling means
spend ages out gradually rather than resetting everyone at midnight on the 1st.
Exceeding your ceiling returns 429 with kind
concurrency_limit_reached, and the message
names the exact spend that would unlock the next tier.
Only delivered work is billed
$0.002 is charged once a bundle has actually been returned. Failures, timeouts, rejected fingerprints and backpressure are never charged, and polling a task is free.
There is a flood backstop
Per key, per second, and separate for each kind of call: 100/s for submissions and 1,000/s for polling. Per key rather than per IP, so nothing you do from one address is charged against anyone else behind it — and per second, so an overshoot costs you a second rather than the rest of a minute.