Lemin task at a glance
LeminTokenProxyLess and LeminToken solve Lemin Cropped Captcha and return two strings that only work together: answer and challenge_id.
| Field | Required | Notes |
|---|---|---|
type |
yes | LeminTokenProxyLess, or LeminToken with your proxy |
websiteURL |
yes | The full page URL that renders the widget |
websiteKey |
yes | The captcha ID, CROPPED_… |
userAgent |
yes | Your browser's User-Agent; use the same one when you submit |
proxy |
LeminToken only |
Your proxy for the solving session |
The Lemin API reference has the request and result examples.
Find the CROPPED captcha ID
The widget script is served from api.leminnow.com/captcha/v1/cropped/<captcha-id>/js. Copy the segment between /cropped/ and /js into websiteKey. Send the identifier itself, not the script URL. This ID configures the widget and differs from the per-challenge challenge_id that the solution returns.
Submit the answer pair
A ready task returns solution.answer and solution.challenge_id. Post them as the form's lemin_answer and lemin_challenge_id values, together, through the page's normal submission. If the site wraps the form in another payload, inspect one ordinary submission to see the final field names.
In browser automation, write both values into the page's hidden inputs before you submit, and keep the page session and userAgent you used for the task.
Widget paths
The solver covers the widget's background completion path and supported interactive cropped sliders. A slider has to finish verification before the API returns a credential, because a puzzle image alone is not a completed answer.
Never mix an answer from one task with a challenge_id from another. When the widget issues a new challenge, request a fresh pair. Failed tasks return their original charge once.
The Lemin guide covers how the two paths differ, a full request-to-submit script and the rejection causes.
Request and ready result
Send this shape to POST /createTask, replacing the page, supported User-Agent and widget ID:
{
"clientKey": "sur_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"task": {
"type": "LeminTokenProxyLess",
"websiteURL": "https://example.com/signup",
"websiteKey": "CROPPED_YOUR_WIDGET_ID",
"userAgent": "YOUR_SUPPORTED_USER_AGENT"
}
}
Poll POST /getTaskResult with clientKey and the returned taskId. A ready response contains the complete pair:
{
"errorId": 0,
"status": "ready",
"solution": {
"answer": "<verified-answer>",
"challenge_id": "<matching-challenge-id>"
}
}
websiteKey configures the widget; challenge_id identifies the completed challenge. Neither can replace the other.
Complete solve and submit example
The script creates and polls the task, then posts solution.answer as lemin_answer and solution.challenge_id as lemin_challenge_id in the same request. It refuses a partial pair and checks the application's response after submission.
// lemin-node.js - solve Lemin Cropped Captcha and post the token with the form.
// Install: Node.js 18 or newer (no packages).
// Run: node lemin-node.js
const { setTimeout: sleep } = require('node:timers/promises');
// Your SurSolver API key.
const CLIENT_KEY = 'sur_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
// The page that renders the widget, with its path and query string.
const TARGET_URL = 'https://example.com/page';
// The CROPPED_ widget ID between /cropped/ and /js in the loader URL.
const SITE_KEY = 'YOUR_SITE_KEY';
// The User-Agent the solve runs with; send the result with this same user agent.
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36';
// Current application session as a Cookie header value; used on form submission.
const SESSION_COOKIES = '';
// Where the page posts the form (the form action, or the request in DevTools, Network).
const FORM_URL = 'https://example.com/submit';
// The form's other fields as a JSON object, such as {"username":"demo"}.
const FORM_FIELDS = undefined;
// Text that appears only in a response the site accepted.
const SUCCESS_TEXT = 'PASTE_ACCEPTED_MARKER';
const API = 'https://api.sursolver.com';
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(task) {
const created = await call('/createTask', { clientKey: CLIENT_KEY, task });
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`);
}
// Optional task fields are sent only when they have a value.
function withoutEmpty(fields) {
return Object.fromEntries(
Object.entries(fields).filter(([, value]) => value !== undefined && value !== ''),
);
}
async function main() {
const solution = await solve({
type: 'LeminTokenProxyLess',
websiteURL: TARGET_URL,
websiteKey: SITE_KEY,
userAgent: USER_AGENT,
});
const responseGroups = [{"lemin_answer":"answer","lemin_challenge_id":"challenge_id"}];
const responseFields = responseGroups.find((group) =>
Object.values(group).every((key) => typeof solution[key] === 'string' && solution[key]),
);
if (!responseFields) throw new Error('The solution has no complete credential field set.');
const formFields = Object.fromEntries(
Object.entries(responseFields).map(([field, key]) => [field, solution[key]]),
);
// Send the token the way the page does; some sites name it differently or send JSON.
// The solve ran with USER_AGENT; the submit presents the same one.
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': USER_AGENT,
...(SESSION_COOKIES ? { Cookie: SESSION_COOKIES } : {}),
};
const body = new URLSearchParams({
...(FORM_FIELDS ?? {}),
...formFields,
});
const response = await fetch(FORM_URL, { method: 'POST', headers, body });
const text = await response.text();
if (!response.ok || !text.includes(SUCCESS_TEXT)) {
throw new Error(`The site did not accept the result (HTTP ${response.status}).`);
}
console.log('The site accepted the result.');
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
The Python version uses the same field pair:
# lemin-python.py - solve Lemin Cropped Captcha and post the token with the form.
# Install: pip install requests
# Run: python lemin-python.py
import time
from urllib.parse import urlsplit
import requests
# Your SurSolver API key.
CLIENT_KEY = "sur_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# The page that renders the widget, with its path and query string.
TARGET_URL = "https://example.com/page"
# The CROPPED_ widget ID between /cropped/ and /js in the loader URL.
SITE_KEY = "YOUR_SITE_KEY"
# The User-Agent the solve runs with; send the result with this same user agent.
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36"
# Current application session as a Cookie header value; used on form submission.
SESSION_COOKIES = None
# Where the page posts the form (the form action, or the request in DevTools, Network).
FORM_URL = "https://example.com/submit"
# The form's other fields as a JSON object, such as {"username": "demo"}.
FORM_FIELDS = None
# Text that appears only in a response the site accepted.
SUCCESS_TEXT = "PASTE_ACCEPTED_MARKER"
API = "https://api.sursolver.com"
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
def solve(task):
created = call("/createTask", {"clientKey": CLIENT_KEY, "task": task})
for _ in range(POLL_ATTEMPTS):
result = call("/getTaskResult", {"clientKey": CLIENT_KEY, "taskId": created["taskId"]})
if result["status"] == "ready":
return result["solution"]
time.sleep(3)
raise TimeoutError(f'task {created["taskId"]} was not ready in {POLL_ATTEMPTS} polls')
def main():
session = requests.Session()
if SESSION_COOKIES:
for part in SESSION_COOKIES.split(";"):
if "=" in part:
name, value = part.strip().split("=", 1)
session.cookies.set(name, value, domain=urlsplit(FORM_URL).hostname, path="/")
solution = solve({
"type": "LeminTokenProxyLess",
"websiteURL": TARGET_URL,
"websiteKey": SITE_KEY,
"userAgent": USER_AGENT,
})
response_groups = [{"lemin_answer":"answer","lemin_challenge_id":"challenge_id"}]
response_fields = next((group for group in response_groups
if all(isinstance(solution.get(key), str) and solution[key]
for key in group.values())), None)
if response_fields is None:
raise SystemExit("The solution has no complete credential field set.")
form_fields = {field: solution[key] for field, key in response_fields.items()}
# The solve ran with USER_AGENT; the submit presents the same one.
headers = {"User-Agent": USER_AGENT}
# Send the token the way the page does; some sites name it differently or send JSON.
response = session.post(
FORM_URL,
data={**(FORM_FIELDS or {}), **form_fields},
headers=headers,
timeout=30,
)
session.close()
if not response.ok or SUCCESS_TEXT not in response.text:
raise SystemExit(f"The site did not accept the result (HTTP {response.status_code}).")
print("The site accepted the result.")
if __name__ == "__main__":
main()
Fill FORM_FIELDS with the form's other values, including its current CSRF field. Preserve the submitting session if the application uses authentication cookies; these examples show an ordinary form POST, not a replacement for the site's application protocol.
Troubleshooting
| Symptom | Check | Correction |
|---|---|---|
Invalid websiteKey |
A script URL or challenge ID was supplied | Send the CROPPED_ widget ID |
| Form rejects a ready result | One field is missing or the fields came from different tasks | Send the complete pair from one result |
| Previously valid pair stops working | The widget issued a new challenge | Solve again for the current attempt |
| Script posts successfully but the app rejects | Field names, session or CSRF differ | Compare with the application's own form submission |