Solve captchas with the Captcha API

Send a task to the Captcha API, read the answer from solution.token, poll a slow solve, solve through your browser session and use the token in Playwright.

Updated

Wayfern solves a captcha task and returns a token. The answer is always in solution.token. This guide shows how to send a task, where each page reads the token, which IP the solver uses, how to wait for a slow solve, the errors and the cost, and how to use the token in a browser session.

Send a task

Send POST /api/captcha/solve with a wf_ API token that has the captcha scope. The body has only a task, with a type and the fields of that type.

curl -X POST https://api.wayfern.com/api/captcha/solve \
  --max-time 120 \
  -H "Authorization: Bearer $WAYFERN_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "task": {
      "type": "recaptcha_v2",
      "websiteURL": "https://www.example.com/login",
      "websiteKey": "6Lc..."
    }
  }'
{
  "id": "cap_6f1c2d8e9b7a4c3f8e2d1b0a9c8d7e6f",
  "status": "ready",
  "type": "recaptcha_v2",
  "sessionId": null,
  "solution": {
    "token": "03AGdBq25..."
  }
}

solution.token is there for each type. Put it where the page reads it.

Where the page reads the token

CaptchaWhere the page reads solution.token
reCAPTCHA v2 and v3The g-recaptcha-response field
hCaptchaThe h-captcha-response field, and g-recaptcha-response when the page has it
TurnstileThe cf-turnstile-response field
FunCaptchaThe token that the Arkose onCompleted callback gets. Older pages read the fc-token field.
GeeTestVersion 3: geetest_validate. Version 4: pass_token.
Amazon WAFThe value of the aws-waf-token cookie
Friendly CaptchaThe frc-captcha-solution field
ProsopoThe procaptcha-response field
ALTCHAThe altcha field
Image to textThe text input of the form. The token is the text in the image.

A GeeTest page needs more than one value. solution.fields has all of them, with the names that the page submits:

  • Version 3: geetest_challenge, geetest_validate, geetest_seccode
  • Version 4: captcha_id, lot_number, pass_token, gen_time, captcha_output

The solution can also have:

  • solution.userAgent, the user agent that the solver used, when it reports one. Some sites refuse a token that a different user agent submits.
  • solution.respKey, for hCaptcha only, when the solver returns one. Some hCaptcha Enterprise pages read it.

Task types

typeRequiredOptional
recaptcha_v2websiteURL, websiteKeyinvisible, enterprise, enterprisePayload and apiDomain (only with enterprise), dataS, userAgent
recaptcha_v3websiteURL, websiteKey, minScore (0.3, 0.7 or 0.9)action, enterprise, apiDomain
hcaptchawebsiteURL, websiteKeyinvisible, enterprisePayload, userAgent
turnstilewebsiteURL, websiteKeyaction, cData, chlPageData
funcaptchawebsiteURL, websiteKey (the Arkose public key)subdomain, data, userAgent
geetestwebsiteURL, gtversion (3 or 4, default 3), challenge, apiServerSubdomain, initParameters, userAgent
amazon_wafwebsiteURL, websiteKey, iv, contextchallengeScript, captchaScript
friendly_captchawebsiteURL, websiteKeyNone
prosopowebsiteURL, websiteKeyNone
altchawebsiteURL, and challengeURL or challengeJSONNone
image_to_textbodyphrase, case, numeric, math, minLength, maxLength, comment
  • Any other field fails with 400 validation, in task or next to it.
  • websiteURL, challengeURL, challengeScript and captchaScript must be absolute http or https URLs.
  • websiteURL is the full URL of the page that shows the captcha. Send page.url().
  • userAgent is the navigator.userAgent of the browser that submits the token. Only recaptcha_v2, hcaptcha, funcaptcha and geetest take it.
  • enterprisePayload, initParameters and challengeJSON are JSON objects. For hCaptcha Enterprise, send { "rqdata": "..." }.
  • apiDomain is www.google.com or www.recaptcha.net.
  • dataS is the data-s value of Google pages. data is a JSON string with the FunCaptcha blob.
  • GeeTest version 3 needs a challenge. Get a new one for each solve. initParameters is for version 4.
  • For Amazon WAF, read websiteKey, iv and context from window.gokuProps on the page.
  • Send ALTCHA challengeURL or challengeJSON, not both.
  • image_to_text takes the image as base64 in body, with no data: prefix, at most 500,000 bytes decoded. numeric is 0 for any characters, 1 for digits only and 2 for no digits.

Solve through your browser session

Some sites check that the token comes from the IP that submits it. When one of your browser sessions has a page open on the websiteURL of the task, Wayfern solves through the proxy of that session: its Wayfern residential exit with the same sticky session, or the link’s own proxy. The token then comes from the IP that the page submits from. Send websiteURL as page.url(), the full URL of the page that shows the captcha.

  • Wayfern checks the open sessions of browser links in each project that the account of your API token is a member of.
  • Wayfern compares the URLs in this order: the same URL (the #fragment is ignored), then the same origin and path, then the same origin. It uses the best level that has a match.
  • When two or more sessions match at that level and use different proxies, the call fails with 409 ambiguous_session. Send the full page.url(). It is usually unique, because login pages have a state value in the URL.
  • When no session matches, the solver uses its own network.
  • Sessions on the Wayfern default datacenter exit, and sessions on an own proxy with https://, also solve from the solver’s own network.
  • recaptcha_v3 and image_to_text always solve from the solver’s own network.
  • When you do not send userAgent and a session matches, Wayfern sends the user agent of that session.

The solver can be slow, or fail to connect, through the proxy of a session. When that solve fails, or has no answer after 90 seconds, Wayfern also solves from the solver’s own network and returns the first answer.

sessionId in a 200 response is the session whose proxy the answer came through. It is null when the answer came from the solver’s own network. In a 202 response it is the session that the solver tries first.

{
  "id": "cap_6f1c2d8e9b7a4c3f8e2d1b0a9c8d7e6f",
  "status": "ready",
  "type": "hcaptcha",
  "sessionId": "0c2c3a1e-5b7d-4f9a-8e21-3d6c9b0a4f17",
  "solution": {
    "token": "P1_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."
  }
}

Wait for the answer

The POST waits about 90 seconds for the answer. When the task is not ready, it returns 202:

{ "id": "cap_6f1c2d8e9b7a4c3f8e2d1b0a9c8d7e6f", "status": "processing", "type": "hcaptcha", "sessionId": null }

Then send GET /api/captcha/solve/:id with the same token. Each GET waits up to 20 seconds, then returns 200 with the solution, 202 again, or an error. Send the next GET after each 202.

curl https://api.wayfern.com/api/captcha/solve/cap_6f1c2d8e9b7a4c3f8e2d1b0a9c8d7e6f \
  --max-time 30 \
  -H "Authorization: Bearer $WAYFERN_API_TOKEN"
  • A task that has no answer 5 minutes after you create it fails with 503 timeout.
  • A result stays readable for 10 minutes.
  • The id of a task of a different account returns 404.

Errors

The Captcha API uses the same flat envelope as the Web Data and Search APIs: error, code, request_id and message, and reason on a 402. Branch on error.

StatuserrorCause
400validationWrong type, a missing or unknown field, or a bad value
401unauthorizedThe token is missing, not valid, or has no captcha scope
402billing_blockedreason is insufficient_credits, spend_limit_reached or no_access
404not_foundUnknown task id
409ambiguous_sessionTwo or more browser sessions match websiteURL and use different proxies. Send the full page.url().
422invalid_site_keyThe site key is wrong, or is for a different captcha type
422invalid_domainThe site key does not belong to the domain of websiteURL
422invalid_taskThe solver refused the task data: an expired challenge, a wrong invisible, a bad image
422unsolvableThe captcha could not be solved. Send a new task.
429rate_limitedMore than 5 open tasks, or more than 20 solve requests a minute
503service_unavailableCaptcha solving is not available for a short time. Retry with backoff.
503timeoutNo answer in 5 minutes. Send a new task.

Cost and limits

  • A solved task costs 1 credit, for each type. At the list rate, that is $0.0125.
  • A task that fails costs nothing.
  • An account can have 5 open tasks. A task is open until it is ready or fails.
  • POST /api/captcha/solve takes 20 requests a minute. GET /api/captcha/solve/:id takes 120 a minute.

Use the token in a browser session

Read the site key from the page and send page.url() as websiteURL. The solver then uses the proxy of this session. Put the token where the page reads it, call the widget callback if the page has one, then submit.

import { chromium } from "playwright";

const browser = await chromium.connectOverCDP(
  `wss://api.wayfern.com/ws?token=${process.env.WAYFERN_LINK_TOKEN}&country=GB&session=acct1`,
  { timeout: 120_000 },
);
const page = await browser.contexts()[0].newPage();
await page.goto("https://www.example.com/login");

const websiteKey = await page.getAttribute(".h-captcha[data-sitekey]", "data-sitekey");

async function solve(body) {
  const headers = {
    Authorization: `Bearer ${process.env.WAYFERN_API_TOKEN}`,
    "Content-Type": "application/json",
  };
  let res = await fetch("https://api.wayfern.com/api/captcha/solve", {
    method: "POST", headers, body: JSON.stringify(body),
  });
  let data = await res.json();
  while (res.status === 202) {
    res = await fetch(`https://api.wayfern.com/api/captcha/solve/${data.id}`, { headers });
    data = await res.json();
  }
  if (!res.ok) throw new Error(`${data.error}: ${data.message}`);
  return data.solution;
}

const { token } = await solve({
  task: { type: "hcaptcha", websiteURL: page.url(), websiteKey },
});

await page.evaluate((token) => {
  for (const name of ["h-captcha-response", "g-recaptcha-response"]) {
    for (const field of document.querySelectorAll(`[name="${name}"]`)) field.value = token;
  }
  const widget = document.querySelector(".h-captcha[data-callback]");
  const callback = widget && window[widget.dataset.callback];
  if (typeof callback === "function") callback(token);
}, token);
await page.click("button[type=submit]");
  • Solve immediately before you submit. A token expires after about 2 minutes and works one time.
  • A page can render the widget from JavaScript with a callback and no data-callback. Then the page reads the token only when that callback runs. Find how the page submits the token (a form field, a hidden input or a request body) and send it the same way.
  • For hCaptcha Enterprise, send the rqdata that the page gives the widget as enterprisePayload.rqdata. Read it again for each solve.
  • Live view and replays show what the page did with the token. See Live view and replays.

The Captcha API needs a wf_ API token. The link token opens the browser only. To solve through the session, the account of the API token must be a member of the project of the link. See the Captcha API reference and captcha limits.