Skip to content
getsolvr

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.

base
https://app.getsolvr.io/api
auth
X-API-Key header
content-type
application/json
providers
kasada, datadome
Contents

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.

both are accepted
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

POST

/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.

POST /api/tasks 202 · accepted
$ 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"
}
GET /api/tasks/{task_id} polling
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

POST

/api/kasada/payload

available

The 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.

POST /api/kasada/payload request
$ 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"
    }'
200 · transformed
{
  "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)

POST

/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.
POST /api/kasada/cd request
$ 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
    }'
200 · free
{
  "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

POST

/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.

POST /api/datadome/solve request · interstitial
$ 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…"
    }'
200 · solved
{
  "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

GET

/api/balance

Current balance and the live per-provider price, including any promotion you qualify for. Free to call and never billed.

GET /api/balance 200
{
  "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.

headers
Solvr-Event:     task.completed
Solvr-Delivery:  8c1d4e77-2a90-4b13-bf65-…
Solvr-Signature: t=1699999999,v1=<hex>
Content-Type:    application/json
verify python
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

live

We 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 code one command
$ claude mcp add --transport http getsolvr \
    https://app.getsolvr.io/mcp \
    --header "X-API-Key: solv_live_9f2c…"
cursor / claude desktop mcp.json
{
  "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.

Ready to send one? $0.002 a solve, and failures don't count.

Create an account