Call the solver from a Next.js Route Handler

By solvethree4 min read

Use a server-side POST handler to create the task once, then retrieve its ID through GET. Keep the solvethree key on the server; the browser shouldn't call the solver directly or create another task whenever a wait expires.

On this page

Set up the server and site profile

  • Use Node.js 22+ and the official JavaScript SDK. Follow the package-URL installation in the documentation; don't assume an npm registry package is available.
  • Set SOLVETHREE_API_KEY, CAPTCHA_SITE, SOLVETHREE_PROXY, and INTEGRATION_KEY in the server environment. Don't give either secret a NEXT_PUBLIC_ prefix.
  • Activate a profile for the target hostname in your workspace before calling the route. Setting CAPTCHA_SITE doesn't create a profile; see site profile setup.
  • Replace the example's login action with the action required by your integration. The request uses one proxy and defaults to reCAPTCHA Enterprise v3; it doesn't need type or url fields.

Add the create-and-retrieve handler

Place the example in app/api/captcha/route.js. It exports App Router HTTP handlers and explicitly selects the Node.js runtime for the SDK. Keep it as a request-time server route, not a statically generated resource.

app/api/captcha/route.js · Node.js Route Handler
import { Solvethree, SolvethreeError } from '@solvethree/sdk';
export const runtime = 'nodejs';
const client = new Solvethree({ apiKey: process.env.SOLVETHREE_API_KEY });
function authorized(request) {
  const key = process.env.INTEGRATION_KEY;
  return Boolean(key) && request.headers.get('x-integration-key') === key;
}
function failure(error) {
  return error instanceof SolvethreeError
    ? Response.json({ requestId: error.requestId, taskId: error.taskId }, { status: 502 })
    : Response.json({ error: 'Invalid task input or ID' }, { status: 400 });
}
export async function POST(request) {
  if (!authorized(request)) return new Response('Unauthorized', { status: 401 });
  try {
    return Response.json(await client.createTask({
      site: process.env.CAPTCHA_SITE, action: 'login', proxy: process.env.SOLVETHREE_PROXY,
      requestId: request.headers.get('x-request-id') ?? undefined,
    }, { waitMs: 0, signal: AbortSignal.timeout(20000) }));
  } catch (error) { return failure(error); }
}
export async function GET(request) {
  if (!authorized(request)) return new Response('Unauthorized', { status: 401 });
  const id = new URL(request.url).searchParams.get('id');
  if (!id) return Response.json({ error: 'Provide id' }, { status: 400 });
  try {
    return Response.json(await client.getTask(id, { waitMs: 15000, signal: AbortSignal.timeout(25000) }));
  } catch (error) { return failure(error); }
}

POST calls createTask with waitMs: 0, so it returns the task response without waiting for a solution. Save the returned task ID. GET requires that existing ID and calls getTask; it never creates a task.

GET requests a wait of up to 15 seconds, with a separate 25-second request abort. POST has a 20-second request abort. These bounds limit each request, not the task's lifetime.

Authorize each task before returning it

For browser access, use your application's real server-side authentication instead of distributing INTEGRATION_KEY. Authentication alone isn't enough: accepting any task ID from an authenticated user would still let them request someone else's result.

  1. 1.

    Associate creation with the caller

    After an authenticated POST creates a task, store its ID against the authenticated user or tenant before returning it. Associate the logical requestId with that owner too, so recovery doesn't lose the ownership record.

  2. 2.

    Check ownership before retrieval

    On GET, authenticate the caller and check the supplied ID against that ownership record before calling getTask. Reject IDs the caller doesn't own, and return results only after that check succeeds.

Keep the site, action, and proxy under server control unless your application explicitly authorizes callers to choose them. The fixed environment values in this example prevent the client from changing those inputs.

Poll the same task and recover uncertain requests

  1. 1.

    Keep polling nonterminal tasks

    For pending or solving, retrieve the same ID again, subject to your application's overall deadline. A completed GET wait isn't a reason to create another task.

  2. 2.

    Stop on ready or failed

    Only ready contains a usable solution. For failed, stop polling and inspect errorCode and error; failed solves are refunded. Handle non-success HTTP responses before interpreting a response as a task. The route's catch handler doesn't replace checking the returned task status.

  3. 3.

    Recover creation after a connection timeout

    Choose and retain an x-request-id before POST. If you received a task ID, retrieve it. If the creation response was lost, repeat POST with the same request ID and unchanged task inputs to recover the existing task. Request IDs accept 1–64 ASCII letters, numbers, underscores, or hyphens; reusing one for different inputs conflicts.

A ready task means solvethree generated a solution, not that the target accepted it. The SDK doesn't submit it for you. When submitting, use the returned userAgent and secChUa with the successful proxy—the configured proxy in this single-proxy example. Don't switch to an unrelated browser identity or IP.

For request errors or terminal failures, use the API error reference before paying for another solve. Retrying unchanged invalid inputs won't fix them.

Run your first task.

Start with $5 in workspace credit.

Create a workspace