You're automating a login flow on Microsoft, EA, Roblox, or GitHub — and suddenly you see a puzzle asking you to rotate an image until it's upright. That's FunCaptcha, built by Arkose Labs.
Unlike reCAPTCHA or hCaptcha, FunCaptcha doesn't use checkboxes or image grids. It's a completely different beast. Let's dig into how it works and how to handle it programmatically.
What Is FunCaptcha?
FunCaptcha presents interactive visual puzzles:
- Rotate an image to the correct orientation
- Match objects by dragging them into position
- Pick the correct image from a set of distorted options
The key difference from other CAPTCHAs: these puzzles require spatial reasoning, making them harder for simple bots but also harder for traditional OCR approaches.
Where You'll Find It
FunCaptcha protects some of the biggest platforms:
| Platform | Where It Appears |
|---|---|
| Microsoft/Outlook | Account creation, login |
| EA (Electronic Arts) | Login, account recovery |
| Roblox | Signup, login |
| GitHub | Certain auth flows |
| Account verification | |
| Snapchat | Registration |
How FunCaptcha Works Under the Hood
When a page loads FunCaptcha, here's what happens:
- Script loads from Arkose Labs CDN
- Browser fingerprinting collects ~150 data points
- Risk assessment decides puzzle difficulty (0-5 rounds)
- Puzzle served via an iframe with encrypted challenge data
- Response token generated after solving
<!-- Typical FunCaptcha embed -->
<div id="funcaptcha" data-pkey="PUBLIC_KEY_HERE"></div>
<script src="https://client-api.arkoselabs.com/fc/api/?onload=setupFC">
</script>
Notice: FunCaptcha uses data-pkey (public key), not data-sitekey. This is a common source of confusion.
Detecting FunCaptcha on a Page
from bs4 import BeautifulSoup
import re
def detect_funcaptcha(html: str) -> dict | None:
soup = BeautifulSoup(html, "html.parser")
# Method 1: Look for the data-pkey attribute
fc_div = soup.find(attrs={"data-pkey": True})
if fc_div:
return {
"type": "funcaptcha",
"public_key": fc_div["data-pkey"]
}
# Method 2: Check for Arkose Labs script
for script in soup.find_all("script", src=True):
if "arkoselabs.com" in script["src"]:
# Extract public key from URL or page JS
pk_match = re.search(
r'data-pkey=["\']([^"\']+)', html
)
if pk_match:
return {
"type": "funcaptcha",
"public_key": pk_match.group(1)
}
# Method 3: Look for enforcement script inline
for script in soup.find_all("script"):
if script.string and "arkoselabs" in script.string:
pk_match = re.search(
r'publicKey\s*[:=]\s*["\']([^"\']+)',
script.string
)
if pk_match:
return {
"type": "funcaptcha",
"public_key": pk_match.group(1)
}
return None
Extracting the Public Key with Playwright
Sometimes the public key isn't in the HTML — it's loaded dynamically. Intercept the network request:
from playwright.sync_api import sync_playwright
def extract_funcaptcha_key(url: str) -> str:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
public_key = None
def on_request(request):
nonlocal public_key
req_url = request.url
if "arkoselabs.com" in req_url:
# Public key is often in the URL path
import re
match = re.search(
r'/fc/gt2/public_key/([^/]+)', req_url
)
if match:
public_key = match.group(1)
page.on("request", on_request)
page.goto(url, wait_until="networkidle")
browser.close()
return public_key
Solving FunCaptcha via API
Once you have the public key, the solving process is similar to other CAPTCHA types — but with different parameters:
import httpx
import time
def solve_funcaptcha(
public_key: str,
page_url: str,
service_url: str = None,
blob_data: str = None
) -> str:
"""
Solve FunCaptcha/Arkose Labs challenge.
Args:
public_key: The data-pkey value
page_url: URL where the CAPTCHA appears
service_url: Arkose Labs API endpoint (optional)
blob_data: Additional challenge data (optional)
"""
client = httpx.Client(base_url="https://www.passxapi.com")
payload = {
"type": "funcaptcha",
"publickey": public_key,
"pageurl": page_url,
}
# Some implementations need the service URL
if service_url:
payload["surl"] = service_url
# Blob data for extra verification
if blob_data:
payload["data"] = blob_data
# Submit task
task = client.post("/api/v1/task", json=payload).json()
task_id = task["task_id"]
# Poll for result (FunCaptcha can take 10-30s)
for _ in range(90):
result = client.get(f"/api/v1/task/{task_id}").json()
if result["status"] == "completed":
return result["token"]
if result["status"] == "failed":
raise Exception(f"Solve failed: {result.get('error')}")
time.sleep(2)
raise TimeoutError("FunCaptcha solve timed out")
The Tricky Part: Blob Data
Some FunCaptcha implementations (especially Microsoft) require blob data — an encrypted payload that the Arkose Labs script generates during initialization. Without it, your token may be rejected.
Here's how to capture it:
def extract_blob_data(page) -> str | None:
"""Extract blob data from Arkose Labs initialization."""
blob = None
def intercept_request(request):
nonlocal blob
if "fc/gt2/public_key" in request.url:
post_data = request.post_data
if post_data and "blob" in post_data:
import urllib.parse
params = urllib.parse.parse_qs(post_data)
if "blob" in params:
blob = params["blob"][0]
page.on("request", intercept_request)
# Trigger the CAPTCHA to load
page.wait_for_selector(
"iframe[data-e2e='enforcement-frame']",
timeout=10000
)
page.remove_listener("request", intercept_request)
return blob
Injecting the Solved Token
After solving, you need to inject the token back into the page:
def inject_funcaptcha_token(page, token: str):
page.evaluate(f"""() => {{
// Method 1: Set the hidden input
const input = document.querySelector(
'input[name="fc-token"]'
);
if (input) {{
input.value = '{token}';
input.dispatchEvent(new Event('change'));
}}
// Method 2: Call the verification callback
if (window.FC_callback) {{
window.FC_callback('{token}');
}}
// Method 3: Trigger Arkose's own callback
if (window.ArkoseEnforcement) {{
window.ArkoseEnforcement
.setConfig({{ onCompleted: null }});
}}
}}""")
Full Example: Microsoft Account Login
import httpx
from playwright.sync_api import sync_playwright
def microsoft_login(email: str, password: str):
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto("https://login.live.com/")
# Enter email
page.fill('input[name="loginfmt"]', email)
page.click('input[type="submit"]')
page.wait_for_timeout(2000)
# Enter password
page.fill('input[name="passwd"]', password)
page.click('input[type="submit"]')
page.wait_for_timeout(3000)
# Check if FunCaptcha appeared
fc_frame = page.query_selector(
"iframe[data-e2e='enforcement-frame']"
)
if fc_frame:
print("FunCaptcha detected, solving...")
# Extract public key from iframe src
src = fc_frame.get_attribute("src")
import re
pk_match = re.search(r'public_key/([^/&]+)', src)
public_key = pk_match.group(1)
# Extract blob data
blob = extract_blob_data(page)
# Solve
token = solve_funcaptcha(
public_key=public_key,
page_url=page.url,
blob_data=blob
)
# Inject token
inject_funcaptcha_token(page, token)
page.wait_for_timeout(3000)
print(f"Current URL: {page.url}")
browser.close()
Common Pitfalls
-
Confusing
pkeywithsitekey— FunCaptcha uses public keys, not sitekeys - Missing blob data — Microsoft implementations almost always need it
- Service URL mismatch — some sites use custom Arkose endpoints, not the default
- Token format — FunCaptcha tokens are much longer than reCAPTCHA tokens (~2KB)
- Rate limiting — Arkose Labs tracks solve patterns; add random delays between attempts
When to Expect FunCaptcha
FunCaptcha typically triggers on:
- New account creation
- Login from new IP/device
- Password reset flows
- After multiple failed attempts
- High-risk actions (payment, settings changes)
Wrapping Up
FunCaptcha is one of the trickier CAPTCHAs to handle because of its interactive nature and the blob data requirement. The key is understanding the three components: public key, service URL, and blob data.
For a clean Python integration that handles all of this, check out passxapi-python — it supports FunCaptcha alongside reCAPTCHA, hCaptcha, and Turnstile with a unified interface.
Running into a specific FunCaptcha implementation that's giving you trouble? Let me know in the comments.
Top comments (0)