Submit a CAPTCHA solution with Playwright for Python

By solvethree3 min read

Solve the CAPTCHA first, then create a Playwright context using the same proxy and the returned browser identity. Submit solution.token through the field your application actually sends.

The example below submits a form on a page you control. It waits for the application’s POST response; it doesn’t intercept reCAPTCHA or depend on a universal CAPTCHA callback.

On this page

Prepare the target and environment

Use Python 3.10+, install Playwright and its Chromium browser, and install the official solvethree Python SDK wheel. Keep the workspace API key in the Python process, not in browser JavaScript.

  • TARGET_URL: the full URL of your form page. The example extracts its hostname for the solve request.
  • SOLVETHREE_PROXY: the proxy URL, including authentication credentials if required. The same proxy is used for solving and browser traffic.
  • SOLVETHREE_API_KEY: your workspace API key.

The target hostname needs an active profile available to your workspace. Set up the site profile before running the example. Change the example’s login action to the action your integration requires; sending a hostname and action doesn’t create a profile. The request defaults to reCAPTCHA Enterprise v3.

Solve, configure Chromium, and submit

Python Playwright · a real form submission
# Install playwright and the official Python SDK; run playwright install chromium.
import asyncio
import os
from urllib.parse import urlsplit, unquote
from playwright.async_api import async_playwright
from solvethree import Solvethree

async def main():
    target = os.environ["TARGET_URL"]
    proxy = urlsplit(os.environ["SOLVETHREE_PROXY"])
    client = Solvethree(os.environ["SOLVETHREE_API_KEY"])
    task = {"site": urlsplit(target).hostname, "action": "login", "proxy": os.environ["SOLVETHREE_PROXY"]}
    result = await asyncio.to_thread(client.solve, task, timeout=180)
    solution = result["solution"]
    async with async_playwright() as playwright:
        browser = await playwright.chromium.launch()
        try:
            netloc = proxy.netloc.rsplit("@", 1)[-1]
            context = await browser.new_context(
                proxy={"server": f"{proxy.scheme}://{netloc}", "username": unquote(proxy.username or ""), "password": unquote(proxy.password or "")},
                user_agent=solution["userAgent"], extra_http_headers={"sec-ch-ua": solution["secChUa"]})
            page = await context.new_page()
            await page.goto(target)
            field = page.locator('[name="g-recaptcha-response"]')
            await field.wait_for(state="attached")
            await field.evaluate("(field, token) => { field.value=token; field.dispatchEvent(new Event('input',{bubbles:true})); }", solution["token"])
            async with page.expect_response(lambda r: r.request.method == "POST" and urlsplit(r.url).netloc == urlsplit(target).netloc) as outcome:
                await page.get_by_role("button", name="Submit", exact=True).click()
            print("Application HTTP status:", (await outcome.value).status)
            # Check the application's actual response; adapt form/CSRF/session setup.
            await context.close()
        finally:
            await browser.close()

asyncio.run(main())

Solvethree is synchronous. Calling client.solve through asyncio.to_thread keeps its create-and-poll operation from blocking Playwright’s event loop. It returns a ready result or raises; the SDK has no native async client.

The browser context uses solution.userAgent and solution.secChUa. The example also separates the proxy server from its username and password, decoding credentials before passing them to Playwright. Because this task uses one proxy, there’s no candidate-selection step.

  1. 1.

    Match the application’s token input

    The example waits for the field to be attached, assigns solution.token, and dispatches an input event. It doesn’t require the field to be visible. If your application submits a different field or reads token state elsewhere, adapt this handoff to match its outgoing request.

  2. 2.

    Add the form’s session requirements

    The example starts with a new browser context. Add any session, form preparation, or CSRF handling your application requires before submitting.

  3. 3.

    Wait for the correct POST

    The response wait is registered before clicking Submit. Its predicate matches a POST to the target URL’s network location. If the page sends other POST requests there, narrow the predicate to your submission endpoint.

Read the application response

The printed HTTP status confirms that a matching response arrived. Check the application’s response body or resulting page state to determine whether it accepted the submission. A ready solvethree task means a solution was generated, not that the target accepted it.

The application rejects a ready token.

Inspect the outgoing form request before paying for another solve.

Try this: Confirm it contains solution.token rather than the task ID or an old value. Check that the submission uses the solve’s proxy, returned user agent and client hints, and intended target.

The response wait captures an unrelated request.

Compare the captured POST URL with the actual form endpoint.

Try this: Make the response predicate specific to that endpoint instead of matching every POST to the same network location.

The solve call raises before browser submission.

Distinguish an HTTP request error from a terminal failed task.

Try this: Handle non-success HTTP responses separately. For a failed task, inspect errorCode and error and correct the cause rather than retrying unchanged inputs. The SDK’s solve method already stops on failure.

Run your first task.

Start with $5 in workspace credit.

Create a workspace