How to Solve DataDome CAPTCHA in Python (requests)

datadome captcha in python - How to Solve DataDome CAPTCHA in Python (requests)

To solve a DataDome CAPTCHA in Python, you read the 403 block page, build the challenge address from it, send that address to the CapSkip DataDome solver with datadome(), and repeat your request with the cookie it returns. The answer is a cookie, not a token for a form, and the CapSkip docs say it only works with the User-Agent and address it was earned with. This guide runs the whole loop with the CapSkip Python SDK and requests in about 90 lines. CapSkip runs on your own Windows PC with no per-solve fee, so a retry costs nothing.

What you need

  • CapSkip 3.2.0 or later running on a Windows PC. That release added DataDome solving for the slider and the device check. The setup guide covers the first start.
  • Python 3.10 or later and the capskip package at version 1.5.0 or later. Earlier versions have no datadome method. The code below ran on Python 3.11.7 and 3.14.0 with capskip 1.5.0 and requests 2.34.2. Python 3.10 was not tried here.
  • A PowerShell prompt. The commands use PowerShell syntax.
  • A page that you own or may automate, behind DataDome.
  • An address for the solver. In Local mode CapSkip answers on 127.0.0.1 for that device only. In Server mode it listens on your network address or public IP, so a script on another PC, a VPS or a hosted platform can reach it over the API. Both are set under connection settings.
  • A sticky proxy per task once you move past testing. The cookie is tied to the address that earned it, which Step 3 explains.

How this was tested: the code ran against three mock servers that I wrote, not against a real DataDome site. One copies the checks listed in the DataDome section of the CapSkip API docs, one plays a protected site that hands out a 403 block page and only accepts a cookie that CapSkip issued, with the User-Agent it was issued for, and one plays a forward proxy. The SDK was the real capskip 1.5.0. The CapSkip app was not running on my PC, so nothing here shows how long a real solve takes or what a live DataDome site accepts. The rules about the cookie come from the CapSkip docs. DataDome’s own cookie documentation says the datadome cookie lasts one year and warns against changing its Domain or Path. It does not say what the cookie is bound to.

Step 1: read the block page and build the captcha_url

When DataDome doubts a request, the site answers with status 403, sometimes 401, and a small HTML page instead of the content. That page holds a script object called dd and loads the challenge in an iframe from geo.captcha-delivery.com. The iframe address is what CapSkip needs, and the docs call it the captcha_url. If the page already contains the iframe, you can take its src as it is. Most block pages build the iframe with a script instead, so the function below assembles the same address from the dd object.

The value of rt picks the path. A c means the slider under /captcha/, and an i means the device check under /interstitial/. The t value matters too: fe is a challenge, and bv means DataDome has banned the address that fetched the page. The first request goes out with a plain Chrome User-Agent. That string is never sent to CapSkip, so its Chrome version doesn’t have to match anything. I did not test whether a live DataDome site hands the same challenge to a different User-Agent, so treat that part as untested.

# pip install capskip requests
import json
import re
from urllib.parse import urlencode

import requests

PAGE_URL = "https://www.example.com/"  # a page behind DataDome
BROWSER = {  # for the first request only; the cookie comes with its own User-Agent
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36",
}


def get(headers, via=None):
    proxies = {"http": via, "https": via} if via else None
    return requests.get(PAGE_URL, headers=headers, proxies=proxies, timeout=30)


def captcha_url_from(resp):
    """Build the challenge address from a DataDome block page, or None."""
    if resp.status_code not in (401, 403):
        return None
    found = re.search(r"var\s+dd\s*=\s*(\{[^}]*\})", resp.text)
    if not found:
        return None
    dd = json.loads(found.group(1).replace("'", '"'))
    path = "interstitial" if dd["rt"] == "i" else "captcha"
    query = {"initialCid": dd["cid"], "hash": dd["hsh"], "cid": dd["cookie"]}
    if dd.get("t"):
        query["t"] = dd["t"]  # fe is a challenge, bv means the address is banned
    query.update(referer=PAGE_URL, s=dd["s"])
    for key in ("e", "b"):
        if dd.get(key):
            query[key] = dd[key]
    query["dm"] = "cd"
    return f"https://{dd.get('host', 'geo.captcha-delivery.com')}/{path}/?{urlencode(query)}"

The function returns None for any response that is not a block page, so a script can tell “nothing to solve” from “solved”. Each block page is a fresh challenge. Use its captcha_url soon, and request the page again for the next task instead of reusing an old address.

Step 2: solve it with datadome()

The SDK method is datadome(captcha_url, pageurl, userAgent, proxy), which follows the 2Captcha argument order. The first two are required. In CapSkip, userAgent and proxy are optional. If you send no User-Agent, CapSkip uses the one from its own browser and returns it as userAgent in the result. If you do send one, it must be Chrome for Windows with the same major version as CapSkip’s browser, so leaving it out is the easy choice. The method waits with the longer 300 second timeout that reCAPTCHA uses, because CapSkip passes the challenge in a real browser. The API docs say a solve usually takes 3 to 20 seconds, and I did not time one.

The proxy is the one your later requests will use. Pass it if your script goes out through a proxy, and pass nothing if the requests leave from the machine that runs CapSkip. With a CapSkip on a server, that machine is the server.

from capskip import CapSkip

# Local mode. In Server mode, use your server's address instead.
solver = CapSkip(host="127.0.0.1", port=8080)


def proxy_arg(proxy_url):
    if not proxy_url:
        return None  # your requests leave from the PC that runs CapSkip
    scheme, rest = proxy_url.split("://", 1)
    return {"type": scheme.upper(), "uri": rest}  # e.g. HTTP, user:pass@ip:port


def solve(captcha_url, proxy_url=None):
    # No userAgent argument: CapSkip uses its own browser's and reports it.
    return solver.datadome(captcha_url, PAGE_URL, proxy=proxy_arg(proxy_url))

If the device check can’t be satisfied, DataDome moves on to a slider. According to the CapSkip docs, the one call follows it there, so you don’t need separate code for the two challenges.

Step 3: send the cookie with its User-Agent and address

The result is a dictionary with four fields that matter. The code field is the cookie exactly as DataDome set it, with Max-Age, Domain and the other attributes, which is what a cookie jar wants. The cookie field is the datadome name and value pair alone, which is what a Cookie header wants. The userAgent field is the User-Agent that earned it. A proxy field appears only when you sent no proxy and CapSkip solved through its own DataDome proxy list. The CapSkip docs say DataDome refuses the cookie from another address or with another User-Agent, so send the same User-Agent every time, and when the result names a proxy, send the request through it. That proxy field is only host and port, with no scheme and no login. The function adds http:// and so assumes a list proxy that needs no login. If yours has one, add it.

DataDome can also send a replacement cookie on a later response. The function keeps the newest one, as a browser would.

def send_with_cookie(result, proxy_url=None):
    # The cookie works only from the address and User-Agent that earned it.
    via = proxy_url
    if not via and result.get("proxy"):
        via = "http://" + result["proxy"]  # CapSkip solved through its own list
    headers = {"User-Agent": result["userAgent"], "Cookie": result["cookie"]}
    resp = get(headers, via)
    fresh = resp.cookies.get("datadome")  # DataDome may rotate the cookie
    if fresh:
        headers["Cookie"] = f"datadome={fresh}"
    return resp, headers, via

The cookie is set by hand in the header, so requests doesn’t manage it for you. That is deliberate: the explicit header wins over the session jar, and it keeps the cookie, User-Agent and proxy together in one place. One thing the proxy run showed: the text after the scheme goes to CapSkip as you wrote it, so keep the proxy login free of characters that need percent-encoding. For a socks5 proxy, requests also needs its socks extra, installed with pip install requests[socks].

Step 4: the full working example

The last block puts the DataDome CAPTCHA in Python flow together and adds the retry. Save all four blocks in one file named datadome_demo.py, one after another, and run it with python datadome_demo.py. Each attempt requests the page again, so every try carries a fresh challenge. A failed solve raises ApiException and the loop tries again. A cookie that gets a 403 anyway also sends the loop around, with the new block page.

from capskip import ApiException


def fetch_protected(proxy_url=None):
    for attempt in range(1, 4):
        resp = get(BROWSER, proxy_url)  # a fresh block page, a fresh challenge
        captcha_url = captcha_url_from(resp)
        if captcha_url is None:
            return resp, BROWSER, proxy_url  # no block page on this response
        try:
            result = solve(captcha_url, proxy_url)
        except ApiException as err:
            print(f"attempt {attempt}: {err}")  # the next attempt gets a new page
            continue
        resp, headers, via = send_with_cookie(result, proxy_url)
        if resp.status_code != 200:
            print(f"attempt {attempt}: HTTP {resp.status_code} with the cookie")
            continue
        return resp, headers, via
    raise RuntimeError("DataDome not passed in 3 attempts")


if __name__ == "__main__":
    page, headers, via = fetch_protected()
    print(page.status_code, page.text[:40])
    again = get(headers, via)  # reuse the cookie while it lasts
    print(again.status_code)

The SDK refuses a captcha_url with t=bv before it sends anything, because no solve from that address can pass. That ValidationException is not an ApiException, so the loop doesn’t catch it. Let it surface: it is the signal to change the address and start over. A ban that CapSkip reports instead, as ERROR_PROXY_BANNED, arrives as an ApiException and the loop retries it on the same address, so if you see that error in the output, change the proxy before you run again. The proxy rotation guide covers rotating proxies, and telling a CAPTCHA from a block covers the other case, where the page is not a challenge at all.

In the mock run the script behaved like this:

Mock settingWhat the script did
Slider block page, every solve answeredOne task, then status 200 with the cookie, and 200 again on reuse
Device check block pageThe same code ran: the captcha_url pointed at /interstitial/ and the result was the same
First two solves refusedTwo attempt lines, then status 200 on the third task
First three solves refusedThree attempt lines, then RuntimeError
Result names a proxy, as CapSkip’s own list doesThe cookie request went through that proxy and got 200
Your own proxy passed to the solvein.php received the proxy and its type, and the requests used it
Block page with t=bvValidationException, and the mock CapSkip saw no request at all
Cookie sent with a different User-AgentThe mock site answered 403
Cookie sent from a different addressThe mock site answered 403 for a cookie that CapSkip had issued through a proxy, sent without it

The last two rows test my mock’s rules, which follow the CapSkip docs. They are not evidence about the real service.

Common errors

ErrorCauseFix
ValidationExceptionA captcha_url that is not a DataDome challenge address, a block page with t=bv, a pageurl that is not http or https, or a userAgent that is not Chrome for Windows. The message says which of these it isRebuild the address from a fresh block page, change the address if it is banned, and drop the userAgent argument
ApiException with ERROR_WRONG_USERAGENTThe User-Agent is Chrome for Windows but its major version differs from CapSkip’s browserSend no userAgent and use the one the result reports
ApiException with ERROR_CAPTCHA_UNSOLVABLEThe challenge was not passedRequest the page again and submit its new captcha_url
ApiException with ERROR_PROXY_BANNEDCapSkip refuses a task whose block page says t=bv, per the API docsChange the proxy and request the page again
ApiException with ERROR_PROXY_CONNECT_REFUSEDCapSkip reports it when it cannot connect through the proxy. I did not reproduce itCheck the proxy address, login and type
TimeoutExceptionCapSkip gave no answer within the 300 second wait. It is not an ApiException, so the loop doesn’t catch itCheck that CapSkip is running and solving, then run the script again
NetworkExceptionCapSkip is not running, or the host or port is wrongStart CapSkip and check the connection settings
Still a 403 with the cookieThe request left from another address, used another User-Agent, or the cookie expiredSend the result’s userAgent and proxy every time, or run the loop again with the new block page

Running the solver on a server

The script needs two things from CapSkip: a host and a port. In Local mode that is 127.0.0.1 and your own PC. In Server mode CapSkip listens on your network address or a public IP, so a script on another PC, a VPS or a hosted platform can call it over the same API. Change the host and port in the CapSkip constructor and nothing else changes. A static public IP is recommended for stable remote access. Server mode is still your own hardware and still unmetered. It does open the port to any address, so add a Windows Firewall block rule for the addresses that should not reach it. Remember that the solve runs on that machine, so the cookie is earned from the server’s address. Unless you pass a proxy, the request that carries the cookie has to leave from that server too. With a proxy, pass the same one to the solve and to your requests. The connection settings show how to switch modes.

FAQ

Does the DataDome solver return a token?

No. It returns the datadome cookie. You send that cookie in the Cookie header of the request that was blocked, together with the same User-Agent. There is no form field to fill.

Do I need a proxy for DataDome?

Not to test. One address is fine for a first run. According to the CapSkip docs, the cookie only works from the address that earned it, so once you send many requests, plan one sticky proxy per task and pass that same proxy to the solve and to your requests.

Does this work for both the slider and the device check?

Yes, through the same call. The captcha_url for a slider sits under /captcha/ and the one for a device check under /interstitial/. The CapSkip docs say a device check that escalates to a slider is followed there.

Can a hosted platform run this against my CapSkip?

Yes, through Server mode. Set CapSkip to listen on your network or public IP, point the CapSkip constructor at that host and port, and the loop works the same, with one change: the solve then runs on the server, so give the solve and your requests the same proxy. The solver stays on your hardware.

The short version

A DataDome CAPTCHA in Python takes four moves: read the block page, build the captcha_url, call datadome(), and repeat the request with the cookie, the User-Agent and the address it belongs to. Retrying is free on your own machine, which is what you want from a local captcha solver when a protected site keeps handing you fresh challenges. For the rest of the SDK, see the Python CAPTCHA solver page.