Docs / Guides
Quickstart
Send your first captcha task from curl, Playwright, Puppeteer, Selenium or a browser extension — five steps from an API key to a solved captcha.
SurSolver is an HTTP API. You send the page URL and the parameters your code already reads from the page, and you get back the token, cookies or fields the page expects. There is nothing to install — no SDK, no browser extension, no plugin.
Five steps take you from an empty account to a solved captcha. Steps 1, 2, 4 and 5 are the same for everyone; step 3 is the code you paste. Pick the tool you will send with — the link jumps to that tool’s section in step 3:
Step 1: Create an account and an API key
- Sign up at sursolver.com. One account holds one prepaid key.
- Open Dashboard → API keys and click Create key. Copy the key
when it appears: it is shown once and cannot be retrieved later. It looks like
ssk_live_…; Change on the same page replaces it with a new one. - Top up the balance from the same dashboard. A task is charged when the API accepts it; Billing & packages explains what that means for failures.
The key travels in the request body as clientKey. The solver endpoints have no auth
header, so never put the key in a URL.
Step 2: Know what you are sending
POST /createTask takes one JSON object:
{
"clientKey": "ssk_live_…",
"task": {
"type": "ReCaptchaV2TaskProxyLess",
"websiteURL": "https://example.com/login",
"websiteKey": "6Le..."
}
}
typepicks the solver; the example above solves reCAPTCHA v2 without a proxy.websiteURLis the page the captcha sits on.websiteKeyis the site key you can read from that page’s HTML.- Types that must run from your own IP take proxy fields as well — see Proxies.
The public catalog, GET /v1/task-types (no key needed), lists every type, the exact
field names it accepts and what its answer carries. The API stores your task object as sent.
Step 3: Send it with your tool
The call is the same everywhere; only the tool that makes it differs. Pick yours from the list at the top of this page — the sections below are in the same order.
REST API
Any language with an HTTP client works. This is the whole call from a shell.
-
Keep the key in an environment variable so it stays out of your code:
export SURSOLVER_KEY="ssk_live_…" -
Create the task:
curl -fsS -X POST "https://api.sursolver.com/createTask" \ -H "Content-Type: application/json" \ -d '{ "clientKey": "'"$SURSOLVER_KEY"'", "task": { "type": "ReCaptchaV2TaskProxyLess", "websiteURL": "https://example.com/login", "websiteKey": "6Le..." } }' -
Read
taskIdfrom the reply and continue with step 4. The fields your type takes are inGET /v1/task-types.
Playwright
-
npm install playwright, then submit the task with Playwright’s own request context — no second HTTP library needed:const { request } = require('playwright'); async function createTask() { const api = await request.newContext(); const res = await api.post('https://api.sursolver.com/createTask', { data: { clientKey: process.env.SURSOLVER_KEY, task: { type: 'ReCaptchaV2TaskProxyLess', websiteURL: 'https://example.com/login', websiteKey: '6Le...', }, }, }); if (!res.ok()) throw new Error(`HTTP ${res.status()}`); // transport failure return res.json(); // { errorId, taskId, status } } -
Once step 4 hands you the token, put it into the page and submit the form:
await page.evaluate((token) => { document.querySelector('#g-recaptcha-response').value = token; }, solution.gRecaptchaResponse); -
Got
taskId? Go to step 4. The fields your type takes are inGET /v1/task-types.
Puppeteer
-
Install Puppeteer and send the task with
fetch— Puppeteer drives the browser but has no HTTP client of its own:async function createTask() { const res = await fetch('https://api.sursolver.com/createTask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientKey: process.env.SURSOLVER_KEY, task: { type: 'ReCaptchaV2TaskProxyLess', websiteURL: 'https://example.com/login', websiteKey: '6Le...', }, }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); // transport failure return res.json(); // { errorId, taskId, status } } -
After step 4, hand the token to the open page:
await page.evaluate((token) => { document.querySelector('#g-recaptcha-response').value = token; }, solution.gRecaptchaResponse); -
Head to step 4 with the
taskId; the fields your type takes are inGET /v1/task-types.
Selenium
-
Send the task from the same Python process that drives the browser — Selenium has no HTTP client of its own, so use
requests:import os import requests created = requests.post( "https://api.sursolver.com/createTask", json={ "clientKey": os.environ["SURSOLVER_KEY"], "task": { "type": "ReCaptchaV2TaskProxyLess", "websiteURL": "https://example.com/login", "websiteKey": "6Le...", }, }, timeout=30, ) created.raise_for_status() # a non-200 reply is a transport failure task_id = created.json()["taskId"] -
Once step 4 returns the token, write it into the page:
driver.execute_script( "document.getElementById('g-recaptcha-response').value = arguments[0];", solution["gRecaptchaResponse"], ) -
Continue with step 4; the fields your type takes are in
GET /v1/task-types.
Browser extension
SurSolver ships no extension — the extension here is yours, and it calls the same API from its background service worker (Manifest V3).
-
Ask the service worker for a task when the toolbar button is clicked:
chrome.action.onClicked.addListener(async (tab) => { const { apiKey } = await chrome.storage.local.get('apiKey'); const res = await fetch('https://api.sursolver.com/createTask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientKey: apiKey, task: { type: 'ReCaptchaV2TaskProxyLess', websiteURL: tab.url, websiteKey: '6Le...', }, }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); // transport failure const { taskId } = await res.json(); // Poll getTaskResult (step 4) for that taskId, then inject the token below. });Reading
tab.urlneeds the"tabs"permission; the injection below needs"scripting"and the key is kept with"storage"— all three go inmanifest.json. -
After step 4, inject the token into the tab:
await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: (token) => { document.querySelector('#g-recaptcha-response').value = token; }, args: [solution.gRecaptchaResponse], }); -
Continue with step 4. Keep the key in
chrome.storage.local, never in a content script, and remember that a key shipped inside a published extension is readable by everyone who installs it. The fields your type takes are inGET /v1/task-types.
Step 4: Poll for the answer
createTask returns a taskId immediately; the solver works in the background. Ask
POST /getTaskResult for that id — with the taskId from step 3:
curl -fsS -X POST "https://api.sursolver.com/getTaskResult" \
-H "Content-Type: application/json" \
-d '{"clientKey":"'"$SURSOLVER_KEY"'","taskId":"tsk_9c1f4e2a7b0d5e3f81a62c47"}'
While the solver is working, the answer is:
{"errorId":0,"status":"processing"}
Poll every 3 seconds until status is ready; the solution object then carries what
the page expects:
{"errorId":0,"status":"ready","solution":{"gRecaptchaResponse":"03AG...","userAgent":"Mozilla/5.0 ..."}}
Polling is not metered by the rate limits. Replies the API reports itself are HTTP 200:
check errorId, and on errorId: 1 stop polling and read errorCode and
errorDescription. A non-200 status or a body without errorId is a transport failure —
see Errors. Results stay readable for 30 minutes from creation; after that the
task answers ERROR_TASK_EXPIRED (see
Task lifecycle).
Step 5: Use the solution
solution carries the values the page needs: usually the token for the widget’s hidden
response field, sometimes cookies for challenge types, plus userAgent when the solver
reported one. Send them from the same exit IP and with the same user agent that produced
them.
POST /getBalance reports the cash balance of the key’s owner in USD:
{"errorId":0,"balance":9.987}
Complete runnable scripts in curl, Python, Node.js and Playwright are at the bottom of this page, generated from the same source as every task page.
Where to go next
- Authentication — how the key authenticates and how it is rotated
- Task lifecycle — what happens between the two calls
- Errors — every error code and how to react
- API reference — the OpenAPI view of these three endpoints
A complete example
A complete, runnable script generated from the same source as the examples in theAPI reference. It runs as-is against https://api.sursolver.comonce your key is in place; change the fields it sends to match your type (GET /v1/task-types lists them).
API="https://api.sursolver.com"
KEY="YOUR_API_KEY"
# 1/2 Create the task. API errors are HTTP 200 with errorId 1; -f also fails on HTTP 5xx.
created=$(curl -fsS -X POST "$API/createTask" \
-H "Content-Type: application/json" \
-d '{"clientKey":"'"$KEY"'","task":{"type":"ReCaptchaV3TaskProxyLess","websiteURL":"https://example.com/login","websiteKey":"6Le..."}}') || { echo "createTask: transport failure" >&2; exit 1; }
echo "$created"
task_id=$(echo "$created" | sed -n 's/.*"taskId": *"\([^"]*\)".*/\1/p')
[ -n "$task_id" ] || { echo "createTask: no taskId (errorId 1)" >&2; exit 1; }
# 2/2 Poll getTaskResult until the task is ready (60 tries, 3 s apart).
attempts=60
while [ "$attempts" -gt 0 ]; do
result=$(curl -fsS -X POST "$API/getTaskResult" \
-H "Content-Type: application/json" \
-d '{"clientKey":"'"$KEY"'","taskId":"'"$task_id"'"}') || { echo "getTaskResult: transport failure" >&2; exit 1; }
if echo "$result" | grep -q '"errorId": *1'; then echo "$result" >&2; exit 1; fi
if echo "$result" | grep -q '"status": *"ready"'; then echo "$result"; exit 0; fi
attempts=$((attempts - 1))
[ "$attempts" -gt 0 ] && sleep 3
done
echo "task $task_id was not ready after 60 polls" >&2
exit 1import time
import requests
API = "https://api.sursolver.com"
CLIENT_KEY = "YOUR_API_KEY"
POLL_ATTEMPTS = 60
def call(path, body):
# API errors are HTTP 200 with errorId 1; anything else is a transport failure.
response = requests.post(f"{API}{path}", json=body, timeout=30)
response.raise_for_status()
data = response.json()
if data["errorId"] != 0:
raise RuntimeError(f'{data["errorCode"]}: {data["errorDescription"]}')
return data
created = call(
"/createTask",
{
"clientKey": CLIENT_KEY,
"task": {
"type": "ReCaptchaV3TaskProxyLess",
"websiteURL": "https://example.com/login",
"websiteKey": "6Le...",
},
},
)
for _ in range(POLL_ATTEMPTS):
result = call(
"/getTaskResult",
{"clientKey": CLIENT_KEY, "taskId": created["taskId"]},
)
if result["status"] == "ready":
break
time.sleep(3)
else:
raise TimeoutError(f'task {created["taskId"]} was not ready in {POLL_ATTEMPTS} polls')
print(result["solution"])const { setTimeout: sleep } = require('node:timers/promises');
const API = 'https://api.sursolver.com';
const CLIENT_KEY = 'YOUR_API_KEY';
const POLL_ATTEMPTS = 60;
// API errors are HTTP 200 with errorId 1; anything else is a transport failure.
async function call(path, body) {
const res = await fetch(`${API}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (data.errorId !== 0) throw new Error(`${data.errorCode}: ${data.errorDescription}`);
return data;
}
async function solve() {
const created = await call('/createTask', {
clientKey: CLIENT_KEY,
task: {
type: 'ReCaptchaV3TaskProxyLess',
websiteURL: 'https://example.com/login',
websiteKey: '6Le...',
},
});
for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt += 1) {
const result = await call('/getTaskResult', {
clientKey: CLIENT_KEY,
taskId: created.taskId,
});
if (result.status === 'ready') return result.solution;
await sleep(3000);
}
throw new Error(`task ${created.taskId} was not ready in ${POLL_ATTEMPTS} polls`);
}
solve().then(
(solution) => console.log(solution),
(error) => {
console.error(error);
process.exitCode = 1;
},
);const { chromium } = require('playwright');
const { setTimeout: sleep } = require('node:timers/promises');
const API = 'https://api.sursolver.com';
const CLIENT_KEY = 'YOUR_API_KEY';
const POLL_ATTEMPTS = 60;
// API errors are HTTP 200 with errorId 1; anything else is a transport failure.
async function call(path, body) {
const res = await fetch(`${API}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (data.errorId !== 0) throw new Error(`${data.errorCode}: ${data.errorDescription}`);
return data;
}
async function solve() {
const created = await call('/createTask', {
clientKey: CLIENT_KEY,
task: {
type: 'ReCaptchaV3TaskProxyLess',
websiteURL: 'https://example.com/login',
websiteKey: '6Le...',
},
});
for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt += 1) {
const result = await call('/getTaskResult', {
clientKey: CLIENT_KEY,
taskId: created.taskId,
});
if (result.status === 'ready') return result.solution;
await sleep(3000);
}
throw new Error(`task ${created.taskId} was not ready in ${POLL_ATTEMPTS} polls`);
}
async function main() {
const browser = await chromium.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com/login');
const solution = await solve();
// Put the token into the captcha's hidden response field, then submit the form.
await page.evaluate(
({ selector, token }) => {
document.querySelector(selector).value = token;
},
{ selector: 'YOUR_RESPONSE_FIELD_SELECTOR', token: solution.gRecaptchaResponse },
);
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});