DEV Community

Alex Chen
Alex Chen

Posted on

FunCaptcha (Arkose Labs): How the Rotate-Image CAPTCHA Works and How to Solve It

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
LinkedIn Account verification
Snapchat Registration

How FunCaptcha Works Under the Hood

When a page loads FunCaptcha, here's what happens:

  1. Script loads from Arkose Labs CDN
  2. Browser fingerprinting collects ~150 data points
  3. Risk assessment decides puzzle difficulty (0-5 rounds)
  4. Puzzle served via an iframe with encrypted challenge data
  5. 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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }});
        }}
    }}""")
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls

  1. Confusing pkey with sitekey — FunCaptcha uses public keys, not sitekeys
  2. Missing blob data — Microsoft implementations almost always need it
  3. Service URL mismatch — some sites use custom Arkose endpoints, not the default
  4. Token format — FunCaptcha tokens are much longer than reCAPTCHA tokens (~2KB)
  5. 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)