Use the CAPTCHA solver in Express

By solvethree3 min read

Keep the solvethree client on your Express server. Use an authenticated POST route to create a task and a GET route to retrieve it. Fix the target and proxy in server configuration so callers can’t turn the endpoint into an arbitrary paid relay.

On this page

Configure the server and target

Use Node.js 22 or later and Express 5. Install the official JavaScript SDK using the tarball installation in the documentation, rather than assuming it’s published in the npm registry. The example creates one Solvethree client outside the handlers.

  • SOLVETHREE_API_KEY: your workspace API key. Keep it on the server.
  • INTEGRATION_KEY: a separate secret for trusted internal callers. The example requires it in the x-integration-key header; don’t distribute it to browser clients. If you already have authentication middleware, use that instead of the example’s shared-key guard.
  • CAPTCHA_SITE: the target hostname, not a full URL. Set up an active site profile for your workspace before creating tasks; sending a hostname doesn’t create one.
  • SOLVETHREE_PROXY: the proxy for this target. The example supplies a single proxy and fixes the action to login. Change that action server-side if your target uses a different one.

This request uses the default reCAPTCHA Enterprise configuration. It needs site, action, and a proxy—not type or url. See site profile setup if the target isn’t configured yet.

Create once, then retrieve the task

Express · authenticated task creation and retrieval
// npm install express and the official solvethree SDK tarball.
import express from 'express';
import { Solvethree, SolvethreeError } from '@solvethree/sdk';
const app = express();
const client = new Solvethree({ apiKey: process.env.SOLVETHREE_API_KEY });
const secret = process.env.INTEGRATION_KEY;
if (!secret) throw new Error('Set INTEGRATION_KEY');
app.use((request, response, next) => {
  if (request.get('x-integration-key') !== secret) return response.sendStatus(401);
  next();
});
app.post('/captcha/tasks', async (request, response) => {
  const task = await client.createTask({
    site: process.env.CAPTCHA_SITE, action: 'login', proxy: process.env.SOLVETHREE_PROXY,
    requestId: request.get('x-request-id'),
  }, { waitMs: 0, signal: AbortSignal.timeout(20000) });
  response.json(task);
});
app.get('/captcha/tasks/:id', async (request, response) => {
  response.json(await client.getTask(request.params.id, { waitMs: 15000, signal: AbortSignal.timeout(25000) }));
});
app.use((error, request, response, next) => {
  if (error instanceof SolvethreeError) return response.status(502).json({ requestId: error.requestId, taskId: error.taskId });
  response.status(400).json({ error: 'Invalid task input or ID' });
});
app.listen(3000);

Both routes sit behind the authentication guard. The POST handler takes the hostname, action, and proxy from server configuration, not the request body. Only the optional creation identifier comes from the caller.

  1. 1.

    Assign a request ID before creation

    Send x-request-id with 1–64 ASCII letters, numbers, underscores, or hyphens. Keep it with the logical request so you can recover creation if the connection times out.

  2. 2.

    POST to /captcha/tasks once

    The handler calls createTask with waitMs: 0 and returns the task. Keep its ID. A successful response means you have a task, not necessarily a solution.

  3. 3.

    Retrieve status with GET

    Call /captcha/tasks/:id. Continue retrieving while the status is pending or solving; don’t repeat POST to check progress. Stop on ready or failed.

  4. 4.

    Use only a ready solution

    Submit the token with the returned userAgent and secChUa, using the same configured proxy and intended target. The SDK doesn’t submit it for you. A ready task means a solution was generated, not that the target accepted your application request.

Handle timeouts without creating another task

The POST SDK call has a 20-second AbortSignal deadline. GET allows a 15-second task wait inside a 25-second SDK-call deadline. A GET response can still be nonterminal; inspect its status rather than treating the end of the wait as a failed solve.

A creation timeout doesn’t prove that solvethree rejected the request. If you received a task ID, retrieve it. Otherwise, repeat creation with the same request ID and unchanged task inputs to recover the existing task. Reusing that ID with different inputs conflicts. Without a retained request ID, a new POST risks creating separate work.

Express 5 forwards rejected async handlers to the error middleware. The example returns HTTP 502 for SolvethreeError, exposing only its request and task identifiers—not the raw error, credentials, or solution. Its fallback HTTP 400 is generic; it doesn’t precisely classify every non-SDK error, including deadline errors.

Handle HTTP errors before reading a success payload. Separately, a retrieved task can have status failed: inspect its errorCode and error, then stop polling. Failed solves are refunded, but retrying unchanged invalid inputs won’t fix them. Use the API error reference to diagnose the reported failure.

Run your first task.

Start with $5 in workspace credit.

Create a workspace