SurSolver

Lemin captcha solver API

Solve Lemin Cropped Captcha and receive the answer and challenge ID your form needs.

Updated

$0.50 / 1K tasksCharged when a task is accepted

On this page

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:

snippet.json
{
  "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:

snippet.json
{
  "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
// 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
# 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

FAQ

Which Lemin captcha is supported?

The integration supports Lemin Cropped Captcha widgets, including their bypass-puzzle and interactive slider paths. Other products are not covered by this task.

What goes in websiteKey?

Use the captcha ID beginning with CROPPED_, copied from the widget script URL or its configuration. Send the identifier, not the script URL.

Does Lemin require a proxy?

LeminTokenProxyLess solves without a caller proxy. LeminToken requires your proxy. Send the same browser userAgent that will submit the result.

What does the solution contain?

It contains answer and challenge_id as separate strings. Place them in lemin_answer and lemin_challenge_id before submitting the target form.

Can I reuse a solved Lemin answer?

Keep answer paired with its challenge_id and submit promptly. A pair from another challenge or page session may be rejected. Create a fresh task when a new challenge is issued.