Was this helpful?
How to bypass FunCaptcha (Arkose Labs) with Playwright
Technical engineer
FunCaptcha is an interactive captcha: instead of a checkbox, the service shows a puzzle, such as rotating an object or picking the image that matches a description. Only a person can complete it, so scraping and testing scripts send the captcha to a solving service and get a token back.
In this guide, we will walk through the complete cycle of bypassing FunCaptcha in Playwright with Python: extract the public key from the page, obtain a solved token via the 2Captcha API, and write it into the field the form validates.
What You Will Need
- Python 3.7 or higher
- Installed Playwright
- 2Captcha account and API key
- Target page URL
- Optional: proxy if the target site blocks requests from datacenter IPs
Step 1. Installing Dependencies
Install the required libraries and Playwright browsers:
bash
pip install playwright 2captcha-python
playwright install chromium
Step 2. Browser Initialization and Page Navigation
For debugging, always start with visible mode headless=False. This allows you to visually monitor the widget appearance and token injection process.
python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
# Navigate to the target page
page.goto("https://mysite.com/page/with/funcaptcha")
Step 3. Extracting the Public Key
The public key of the site is stored in one of two places. The primary one is the data-pkey attribute of the div element holding the widget. The fallback is the fc-token field: in its value the key follows pk, and the service address follows surl.
python
import re
widget = page.locator("[data-pkey]").first
surl = None
if widget.count() > 0:
pkey = widget.get_attribute("data-pkey")
else:
fc_token = page.locator('[name="fc-token"]').first.get_attribute("value")
pk_match = re.search(r"pk=([^|]+)", fc_token)
if not pk_match:
raise RuntimeError("No pk parameter in the fc-token value")
pkey = pk_match.group(1)
surl_match = re.search(r"surl=([^|]+)", fc_token)
surl = surl_match.group(1) if surl_match else None
page_url = page.url
print(f"Public key: {pkey}")
Important: the count() check is required here. Without it, calling get_attribute on a missing element ends in a timeout rather than an empty value, and execution never reaches the fallback. For the same reason both locators are narrowed with .first: Playwright locators are strict and raise an error on two matches instead of returning the first element.
The service address inside fc-token is written with its scheme, and the SDK expects the value in exactly that form.
Step 4. Sending the Task to the API and Getting the Token
The simplest approach is to use the official Python SDK. It automatically creates a task and polls the server until a result is obtained.
python
import os
from twocaptcha import TwoCaptcha
api_key = os.getenv('APIKEY_2CAPTCHA', 'YOUR_API_KEY')
solver = TwoCaptcha(api_key)
result = solver.funcaptcha(
sitekey=pkey,
url=page_url,
surl=surl or 'https://client-api.arkoselabs.com'
)
token = result['code']
print(f"Token received: {token[:20]}...")
The surl parameter is the address the widget loads from. It is optional: if you leave it out, a default value is used, which works in most cases. Passing it is still recommended.
Some sites need one more parameter, blob. Finding it on the page and making sure it is correct is covered in a separate article: How to find and check the blob parameter. If the site uses a blob, pass the task the same value the widget received. The value works for a limited time, and you get a new one by reloading the page.
python
result = solver.funcaptcha(
sitekey=pkey,
url=page_url,
surl='https://client-api.arkoselabs.com',
userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
proxy={'type': 'HTTPS', 'uri': 'login:password@123.123.123.123:8080'},
**{'data[blob]': 'BLOB_DATA_VALUE'}
)
Alternative (direct HTTP request):
If you don't use the SDK, you can send a request directly via curl or the requests library. Parameters are written differently there: the service address goes into funcaptchaApiJSSubdomain without the scheme, and the blob goes into the data field as a JSON string.
bash
curl -X POST https://api.2captcha.com/createTask \
-H "Content-Type: application/json" \
-d '{
"clientKey": "YOUR_API_KEY",
"task": {
"type": "FunCaptchaTaskProxyless",
"websiteURL": "https://mysite.com/page/with/funcaptcha",
"websitePublicKey": "6220FF23-9856-3A6F-9FF1-A14F88123F55",
"funcaptchaApiJSSubdomain": "client-api.arkoselabs.com",
"data": "{\"blob\":\"BLOB_DATA_VALUE\"}"
}
}'
If the site requires binding to a specific IP, use the FunCaptchaTask task type and add the proxy parameters: proxyType, proxyAddress, proxyPort, proxyLogin, proxyPassword.
Before you build the call into a script, check the parameters in sandbox mode. It does not behave like a normal task: the captcha is not sent to the service workers, it comes back to you. You switch your role to worker, log into the worker application with the key for that role, and solve your own captcha by hand, with nothing charged to your balance.
What this verifies is the parameters. If the captcha opens in the application, websiteURL, the public key and the blob are correct. If it does not, one of them is wrong — an expired blob or the wrong public key, for example. Sandbox mode is switched on in the account settings and becomes available after your first top-up. Turn it off when you are done, or new tasks will keep coming to you instead of the workers. See Debugging for details.
Step 5. Injecting the Token into the Page (Critical Step)
The token is written into the fc-token field. Check several options as in the example below.
The token arrives as a long string containing |, = and %, so pass it into the page as a function argument rather than interpolating it into the script text: string concatenation can break the script itself.
python
page.evaluate("""(token) => {
const field = document.querySelector('[name="fc-token"]')
|| document.querySelector('#fc-token');
if (!field) {
throw new Error('The fc-token field was not found on the page');
}
field.value = token;
}""", token)
The error in this function is deliberate. Without it the script skips the write in silence, submits the form with an empty field and reports success.
Some sites also use a callback function that triggers upon successful solving and unlocks the submit button. Its name depends on the site: look for it in the page source and call it through page.evaluate the same way.
Step 6. Submitting the Form
After successfully injecting the token, the form should consider itself valid. Use your own button selector: button[type='submit'] does not fit every form.
python
page.click("button[type='submit']")
page.wait_for_load_state("networkidle")
browser.close()
Report the result of every solution. The SDK provides the report method, and the API provides reportCorrect and reportIncorrect. The task identifier arrives in the same response as the token: solver.report(result['captchaId'], True).
Complete Working Example (Python)
python
import os
import re
from playwright.sync_api import sync_playwright
from twocaptcha import TwoCaptcha
API_KEY = os.getenv('APIKEY_2CAPTCHA', 'YOUR_API_KEY')
TARGET_URL = "https://mysite.com/page/with/funcaptcha"
def solve_funcaptcha():
solver = TwoCaptcha(API_KEY)
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
try:
page.goto(TARGET_URL)
# 1. Extract the public key and the service address
widget = page.locator("[data-pkey]").first
surl = None
if widget.count() > 0:
pkey = widget.get_attribute("data-pkey")
else:
fc_token = page.locator('[name="fc-token"]').first.get_attribute("value")
pk_match = re.search(r"pk=([^|]+)", fc_token)
if not pk_match:
raise RuntimeError("No pk parameter in the fc-token value")
pkey = pk_match.group(1)
surl_match = re.search(r"surl=([^|]+)", fc_token)
surl = surl_match.group(1) if surl_match else None
print(f"Public key: {pkey}")
# 2. Solve the captcha via API
print("Sending task to API...")
result = solver.funcaptcha(
sitekey=pkey,
url=TARGET_URL,
surl=surl or 'https://client-api.arkoselabs.com'
)
token = result['code']
print("Token received!")
# 3. Inject the token into the fc-token field
page.evaluate("""(token) => {
const field = document.querySelector('[name="fc-token"]')
|| document.querySelector('#fc-token');
if (!field) {
throw new Error('The fc-token field was not found on the page');
}
field.value = token;
}""", token)
# 4. Submit the form
page.click("button[type='submit']")
page.wait_for_load_state("networkidle")
print("Form submitted.")
page.wait_for_timeout(3000) # Pause for visual verification
except Exception as e:
print(f"Error: {e}")
finally:
browser.close()
if __name__ == "__main__":
solve_funcaptcha()
Troubleshooting Common Issues
- Public key not found (returns empty): the markup has no
data-pkeyattribute. Read the key from thefc-tokenfield value, as shown in Step 3. - Script doesn't find the captcha element: the widget may sit inside an iframe. Reach it through
page.frame_locator. - Task is created but no solution arrives: the site may require a
blob, and yours is either missing or expired. Reload the page and read a new value. - Token is injected but the form rejects it: the site validates the solution through its own callback, not only through the contents of the field. Find that callback in the page source and call it through
page.evaluate, as shown in Step 5.
The remaining error codes are listed on the Error Codes page, linked below.
Conclusion
The hard part of this captcha is that its puzzle cannot be parsed programmatically: the service renders an interactive scene rather than text or a grid of images. The FunCaptcha method removes that difficulty completely — a worker solves the puzzle, and your code is left with three actions.
Find the public key, wait for the token, and write it into the fc-token field. The last one deserves attention: the write has to be checked, or an empty field goes out with the form without the script noticing.
Report your results through reportCorrect and reportIncorrect, so the service can tell which solutions did not work.
Useful Links
- 2Captcha API documentation for Arkose Labs captcha: https://2captcha.com/api-docs/arkoselabs-funcaptcha
- How to bypass Arkose Labs captcha: https://2captcha.com/p/funcaptcha
- How to find and check the blob parameter: https://2captcha.com/h/funcaptcha-blob
- Error Codes: https://2captcha.com/api-docs/error-codes
- Debugging & Sandbox: https://2captcha.com/api-docs/debugging
- 2Captcha Python SDK on GitHub: https://github.com/2captcha/2captcha-python