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
| Captcha | Where the page reads solution.token |
|---|---|
| reCAPTCHA v2 and v3 | The g-recaptcha-response field |
| hCaptcha | The h-captcha-response field, and g-recaptcha-response when the page has it |
| Turnstile | The cf-turnstile-response field |
| FunCaptcha | The token that the Arkose onCompleted callback gets. Older pages read the fc-token field. |
| GeeTest | Version 3: geetest_validate. Version 4: pass_token. |
| Amazon WAF | The value of the aws-waf-token cookie |
| Friendly Captcha | The frc-captcha-solution field |
| Prosopo | The procaptcha-response field |
| ALTCHA | The altcha field |
| Image to text | The 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
type | Required | Optional |
|---|---|---|
recaptcha_v2 | websiteURL, websiteKey | invisible, enterprise, enterprisePayload and apiDomain (only with enterprise), dataS, userAgent |
recaptcha_v3 | websiteURL, websiteKey, minScore (0.3, 0.7 or 0.9) | action, enterprise, apiDomain |
hcaptcha | websiteURL, websiteKey | invisible, enterprisePayload, userAgent |
turnstile | websiteURL, websiteKey | action, cData, chlPageData |
funcaptcha | websiteURL, websiteKey (the Arkose public key) | subdomain, data, userAgent |
geetest | websiteURL, gt | version (3 or 4, default 3), challenge, apiServerSubdomain, initParameters, userAgent |
amazon_waf | websiteURL, websiteKey, iv, context | challengeScript, captchaScript |
friendly_captcha | websiteURL, websiteKey | None |
prosopo | websiteURL, websiteKey | None |
altcha | websiteURL, and challengeURL or challengeJSON | None |
image_to_text | body | phrase, case, numeric, math, minLength, maxLength, comment |
- Any other field fails with 400
validation, intaskor next to it. websiteURL,challengeURL,challengeScriptandcaptchaScriptmust be absolute http or https URLs.websiteURLis the full URL of the page that shows the captcha. Sendpage.url().userAgentis thenavigator.userAgentof the browser that submits the token. Onlyrecaptcha_v2,hcaptcha,funcaptchaandgeetesttake it.enterprisePayload,initParametersandchallengeJSONare JSON objects. For hCaptcha Enterprise, send{ "rqdata": "..." }.apiDomainiswww.google.comorwww.recaptcha.net.dataSis thedata-svalue of Google pages.datais a JSON string with the FunCaptchablob.- GeeTest version 3 needs a
challenge. Get a new one for each solve.initParametersis for version 4. - For Amazon WAF, read
websiteKey,ivandcontextfromwindow.gokuPropson the page. - Send ALTCHA
challengeURLorchallengeJSON, not both. image_to_texttakes the image as base64 inbody, with nodata:prefix, at most 500,000 bytes decoded.numericis 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
#fragmentis 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 fullpage.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_v3andimage_to_textalways solve from the solver’s own network.- When you do not send
userAgentand 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.
| Status | error | Cause |
|---|---|---|
| 400 | validation | Wrong type, a missing or unknown field, or a bad value |
| 401 | unauthorized | The token is missing, not valid, or has no captcha scope |
| 402 | billing_blocked | reason is insufficient_credits, spend_limit_reached or no_access |
| 404 | not_found | Unknown task id |
| 409 | ambiguous_session | Two or more browser sessions match websiteURL and use different proxies. Send the full page.url(). |
| 422 | invalid_site_key | The site key is wrong, or is for a different captcha type |
| 422 | invalid_domain | The site key does not belong to the domain of websiteURL |
| 422 | invalid_task | The solver refused the task data: an expired challenge, a wrong invisible, a bad image |
| 422 | unsolvable | The captcha could not be solved. Send a new task. |
| 429 | rate_limited | More than 5 open tasks, or more than 20 solve requests a minute |
| 503 | service_unavailable | Captcha solving is not available for a short time. Retry with backoff. |
| 503 | timeout | No 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/solvetakes 20 requests a minute.GET /api/captcha/solve/:idtakes 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
rqdatathat the page gives the widget asenterprisePayload.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.