Captcha solving service blog | SolveCaptcha DataDome bypass with Python: Complete API guide

DataDome bypass

Title:
How to bypass DataDome captcha with SolveCaptcha

Description:
Learn how to detect a DataDome challenge, extract a fresh captcha URL, submit it to SolveCaptcha with the correct proxy and User-Agent, retrieve the datadome cookie, and reuse it in the original Python or browser session.

H1:
How to bypass DataDome captcha

Content:
DataDome can interrupt scraping, browser automation, QA, and data collection workflows when a request is classified as automated or suspicious. Unlike a basic image captcha, DataDome verification is tied to the browser session, IP address, User-Agent, challenge URL, and the datadome cookie.

With the SolveCaptcha DataDome solver, you can submit the current challenge URL together with the original proxy and User-Agent. SolveCaptcha processes the challenge and returns the cookie required to continue the session.

The complete workflow is:

Open the protected page
→ detect the DataDome challenge
→ extract the current captcha URL
→ verify the t parameter
→ submit the URL, proxy, and User-Agent
→ poll the SolveCaptcha API
→ retrieve and parse the datadome cookie
→ apply it to the original session
→ repeat the protected request

This guide covers:

  • identifying a DataDome block;
  • extracting captcha_url through DevTools, HTML, or a 403 response;
  • detecting blocked proxy IPs;
  • preserving the same proxy and User-Agent;
  • creating and polling a SolveCaptcha task;
  • applying the returned cookie to Requests or Selenium;
  • handling expired challenges, API errors, and browser-fingerprint differences;
  • reporting accepted and rejected solutions.

Use this method only on websites you own or are authorized to test, scrape, or automate.

What is DataDome?

DataDome is a bot-management and fraud-prevention platform used to protect websites, mobile applications, and APIs.

A protected website can evaluate signals such as:

  • IP reputation;
  • request frequency;
  • browser fingerprint;
  • JavaScript execution;
  • cookies;
  • request headers;
  • TLS characteristics;
  • navigation patterns;
  • mouse and keyboard activity;
  • previous behavior associated with the session.

When a request is classified as suspicious, the website may return an HTTP 403 response or display a captcha through an iframe hosted on a DataDome captcha-delivery domain.

A typical captcha URL starts with:

https://geo.captcha-delivery.com/captcha/

After the challenge is completed, DataDome returns a new cookie:

datadome=COOKIE_VALUE

That cookie must be used with the same browser environment and proxy session.

How DataDome captcha solving works

The SolveCaptcha DataDome method is cookie-based.

The workflow is:

  1. Send a request to the protected website.
  2. Detect the DataDome block.
  3. Extract the captcha URL from the iframe or block response.
  4. Confirm that the captcha URL contains a valid t parameter.
  5. Submit the captcha URL, page URL, proxy, and User-Agent to SolveCaptcha.
  6. Receive a task ID.
  7. Poll the task until the result is ready.
  8. Receive a complete datadome cookie string.
  9. Add the cookie to the original browser or HTTP session.
  10. Retry the protected request using the same proxy and User-Agent.

SolveCaptcha API endpoints

Create a task through:

POST https://api.solvecaptcha.com/in.php

Retrieve the result through:

GET https://api.solvecaptcha.com/res.php

The required method is:

method=datadome

Do not use unsupported task structures or parameter names such as:

DataDomeSliderTask
createTask
getTaskResult
clientKey
websiteURL
captchaUrl
proxyAddress
proxyPort

The SolveCaptcha DataDome method uses:

key
method
captcha_url
pageurl
userAgent
proxy
proxytype

Required DataDome parameters

The API request requires the following values:

Parameter Required Description
key Yes Your SolveCaptcha API key
method Yes Must be datadome
captcha_url Yes Full URL from the captcha iframe src attribute
pageurl Yes Full URL where the captcha appeared
userAgent Yes User-Agent used to access the target website
proxy Yes Proxy in login:password@host:port format
proxytype Yes HTTP, HTTPS, SOCKS4, or SOCKS5
json No Set to 1 to receive JSON responses

A proxy is mandatory for SolveCaptcha DataDome tasks.

The proxy and User-Agent should match those used by the browser or HTTP client that received the challenge.

Why the same proxy is required

A DataDome captcha solution is associated with the IP address used to load and solve the challenge.

The following configuration is incorrect:

Target request: Proxy A
SolveCaptcha task: Proxy B
Retry request: Proxy C

Use:

Target request: Proxy A
SolveCaptcha task: Proxy A
Retry request: Proxy A

Avoid rotating the proxy between:

  • the initial blocked request;
  • captcha URL extraction;
  • SolveCaptcha task creation;
  • result retrieval;
  • cookie application;
  • the retried target request.

If the proxy rotates automatically, use a sticky session.

Why the User-Agent must match

The API requires the exact User-Agent used to access the protected page.

Example:

Mozilla/5.0 (...) Chrome/... Safari/537.36

Use the same value in:

  1. the original website request;
  2. the SolveCaptcha userAgent parameter;
  3. the retry after applying the cookie.

Do not send one User-Agent to DataDome and another to SolveCaptcha.

Use a complete User-Agent from a current browser rather than an abbreviated or invented value.

How to identify and extract a DataDome challenge

Open browser DevTools and inspect the failed request.

Common indicators include:

captcha-delivery.com
geo.captcha-delivery.com
interstitial.captcha-delivery.com
datadome
initialCid
cid
hash
t=fe

A challenge may appear:

  • as a visible iframe;
  • as an interstitial page;
  • after an HTTP 403 response;
  • after a protected button is clicked;
  • during login or registration;
  • after several repeated requests.

A normal HTTP 403 response does not automatically prove that DataDome generated the block. Confirm that the response or loaded resources contain DataDome-related URLs.

Method 1: Find captcha_url in the Elements panel

Open the Elements panel and search for:

captcha-delivery.com

You may find an iframe similar to:

<iframe
  src="https://geo.captcha-delivery.com/captcha/?initialCid=...&hash=...&t=fe&referer=...">
</iframe>

Copy the complete value of the src attribute.

That complete URL becomes:

captcha_url

Do not copy only the domain or path.

The query parameters are part of the current challenge and must be preserved.

Method 2: Find captcha_url in the Network panel

The Network panel is usually the most reliable method.

  1. Open DevTools.
  2. Select Network.
  3. Enable Preserve log.
  4. Clear the existing requests.
  5. Trigger the protected action.
  6. Search for captcha-delivery.
  7. Select the challenge request.
  8. Copy its complete request URL.

Check both document and iframe requests.

The URL may appear after the original target request returns HTTP 403.

Method 3: Find the iframe URL in the browser console

Run:

const captchaFrame = [
  ...document.querySelectorAll(
    "iframe[src]"
  )
].find((iframe) =>
  /captcha-delivery\.com\/captcha/i.test(
    iframe.src
  )
);

if (captchaFrame) {
  console.log(
    captchaFrame.src
  );
} else {
  console.log(
    "DataDome captcha iframe was not found."
  );
}

This script reads only the iframe URL. It does not attempt to access the cross-origin iframe contents.

If the iframe is not present in the top-level document, use the Network panel.

Extract the iframe URL from HTML

The following function finds a DataDome captcha iframe in an HTML response:

import html
import re
from urllib.parse import urljoin

DATADOME_IFRAME_PATTERN = re.compile(
    r"""
    <iframe
    [^>]+
    src=["']
    (
        [^"']*
        captcha-delivery\.com/captcha/
        [^"']*
    )
    ["']
    """,
    flags=re.IGNORECASE | re.VERBOSE,
)

def extract_captcha_iframe_url(
    page_html: str,
    page_url: str,
) -> str | None:
    match = DATADOME_IFRAME_PATTERN.search(
        page_html
    )

    if not match:
        return None

    raw_url = html.unescape(
        match.group(1)
    )

    return urljoin(
        page_url,
        raw_url,
    )

Example:

captcha_url = extract_captcha_iframe_url(
    response.text,
    response.url,
)

if captcha_url:
    print(captcha_url)

Extract DataDome block parameters from a 403 response

Some websites return an inline JavaScript object instead of a fully rendered iframe.

It may resemble:

var dd = {
  cid: "INITIAL_CID",
  hsh: "HASH_VALUE",
  t: "fe",
  s: "SITE_ID",
  e: "ENCRYPTED_VALUE"
};

The response can also set a temporary datadome cookie through the Set-Cookie header.

In that case, the captcha URL can be constructed from:

  • dd.cid;
  • dd.hsh;
  • dd.t;
  • dd.s;
  • dd.e;
  • the temporary datadome cookie;
  • the protected page URL.

Use this only as a fallback when the actual iframe URL is unavailable.

import re
from urllib.parse import urlencode

class DataDomeExtractionError(RuntimeError):
    """Raised when DataDome parameters cannot be extracted."""

def extract_js_property(
    source: str,
    property_name: str,
) -> str | None:
    pattern = re.compile(
        rf"""
        ["']?
        {re.escape(property_name)}
        ["']?
        \s*:\s*
        ["']
        ([^"']+)
        ["']
        """,
        flags=re.IGNORECASE | re.VERBOSE,
    )

    match = pattern.search(source)

    if not match:
        return None

    return match.group(1)

def extract_datadome_configuration(
    page_html: str,
) -> dict[str, str]:
    object_match = re.search(
        r"""
        (?:var\s+)?
        dd
        \s*=\s*
        \{
        (.*?)
        \}
        \s*;?
        """,
        page_html,
        flags=re.IGNORECASE
        | re.DOTALL
        | re.VERBOSE,
    )

    if not object_match:
        raise DataDomeExtractionError(
            "DataDome configuration object was not found"
        )

    source = object_match.group(1)

    values = {
        name: extract_js_property(
            source,
            name,
        )
        for name in (
            "cid",
            "hsh",
            "t",
            "s",
            "e",
        )
    }

    missing = [
        name
        for name, value in values.items()
        if not value
    ]

    if missing:
        raise DataDomeExtractionError(
            "Missing DataDome values: "
            + ", ".join(missing)
        )

    return {
        name: str(value)
        for name, value in values.items()
    }

def build_datadome_captcha_url(
    configuration: dict[str, str],
    temporary_cookie: str,
    page_url: str,
) -> str:
    if configuration["t"] == "bv":
        raise RuntimeError(
            "The current proxy IP is blocked by DataDome"
        )

    if configuration["t"] != "fe":
        raise RuntimeError(
            "Unsupported DataDome challenge type: "
            f"{configuration['t']}"
        )

    query = urlencode(
        {
            "initialCid": configuration["cid"],
            "hash": configuration["hsh"],
            "cid": temporary_cookie,
            "t": configuration["t"],
            "referer": page_url,
            "s": configuration["s"],
            "e": configuration["e"],
        }
    )

    return (
        "https://geo.captcha-delivery.com/"
        f"captcha/?{query}"
    )

Use the temporary cookie from the HTTP response:

temporary_cookie = response.cookies.get(
    "datadome"
)

if not temporary_cookie:
    raise DataDomeExtractionError(
        "Temporary datadome cookie was not found"
    )

configuration = (
    extract_datadome_configuration(
        response.text
    )
)

captcha_url = build_datadome_captcha_url(
    configuration=configuration,
    temporary_cookie=temporary_cookie,
    page_url=response.url,
)

Check the t parameter before submitting the task

Before sending the captcha URL to SolveCaptcha, inspect its t query parameter.

A valid challenge normally contains:

t=fe

Example:

https://geo.captcha-delivery.com/captcha/?initialCid=...&t=fe&referer=...

If the URL contains:

t=bv

the proxy IP has been blocked by DataDome.

Do not submit that challenge repeatedly. Replace the proxy, open the protected page again, and obtain a new captcha URL.

Python validation:

from urllib.parse import parse_qs, urlparse

def validate_captcha_url(captcha_url: str) -> None:
    query = parse_qs(
        urlparse(captcha_url).query
    )

    challenge_type = query.get(
        "t",
        [None],
    )[0]

    if challenge_type == "bv":
        raise RuntimeError(
            "The proxy IP is blocked by DataDome. "
            "Change the proxy and request a new challenge."
        )

    if (
        challenge_type is not None
        and challenge_type != "fe"
    ):
        raise RuntimeError(
            "Unsupported DataDome challenge type: "
            f"{challenge_type}"
        )

If the t parameter is absent, do not invent it. Submit the original iframe URL only when it is a genuine DataDome captcha URL.

Challenge freshness

The complete captcha URL belongs to the current challenge.

It may become invalid when:

  • the page is reloaded;
  • the proxy changes;
  • the browser session changes;
  • the challenge is completed;
  • DataDome generates another challenge;
  • the URL is stored and reused later;
  • the target page changes.

Extract captcha_url immediately before creating the SolveCaptcha task.

Do not cache it for future sessions.

Do not combine:

  • a URL from one proxy;
  • a User-Agent from another browser;
  • a page URL from another route;
  • cookies from another session.

Get your SolveCaptcha API key

Copy your API key from the SolveCaptcha account settings.

Store it in an environment variable.

Linux or macOS:

export SOLVECAPTCHA_API_KEY="YOUR_API_KEY"

Windows PowerShell:

$env:SOLVECAPTCHA_API_KEY="YOUR_API_KEY"

Do not publish the key in source code, screenshots, logs, or public repositories.

Create a SolveCaptcha DataDome task

Example request:

curl --request POST \
  --url https://api.solvecaptcha.com/in.php \
  --data-urlencode "key=YOUR_API_KEY" \
  --data-urlencode "method=datadome" \
  --data-urlencode "captcha_url=https://geo.captcha-delivery.com/captcha/?initialCid=...&t=fe&referer=..." \
  --data-urlencode "pageurl=https://example.com/protected-page" \
  --data-urlencode "userAgent=YOUR_BROWSER_USER_AGENT" \
  --data-urlencode "proxy=login:[email protected]:8000" \
  --data-urlencode "proxytype=http" \
  --data-urlencode "json=1"

A successful response contains the task ID:

{
  "status": 1,
  "request": "2033679236"
}

Save the value from request.

Retrieve the DataDome cookie

Request the result through:

GET https://api.solvecaptcha.com/res.php

Parameters:

key=YOUR_API_KEY
action=get
id=2033679236
json=1

Example:

curl "https://api.solvecaptcha.com/res.php?key=YOUR_API_KEY&action=get&id=2033679236&json=1"

While the task is being processed:

{
  "status": 0,
  "request": "CAPCHA_NOT_READY"
}

Wait five seconds and repeat the request using the same task ID.

A successful response contains the complete cookie string:

{
  "status": 1,
  "request": "datadome=H7LmQvR8kP3D...XyJcTt9LsAbQW2N7FZk; Max-Age=31536000; Domain=.example.com; Path=/; Secure; SameSite=Lax"
}

The response is not only the cookie value. It can also contain attributes such as:

  • Domain;
  • Path;
  • Max-Age;
  • Secure;
  • SameSite.

Parse the response before adding it to the session.

Complete Python DataDome solver

Install Requests:

python -m pip install requests

For SOCKS proxies:

python -m pip install "requests[socks]"

Set the required environment variables:

export SOLVECAPTCHA_API_KEY="YOUR_API_KEY"
export TARGET_URL="https://example.com/protected-page"
export TARGET_PROXY="login:[email protected]:8000"
export TARGET_PROXY_TYPE="http"
export BROWSER_USER_AGENT="YOUR_BROWSER_USER_AGENT"

Create datadome_solver.py:

#!/usr/bin/env python3

from __future__ import annotations

import html
import json
import os
import re
import time
from dataclasses import asdict, dataclass
from http.cookies import SimpleCookie
from pathlib import Path
from typing import Any
from urllib.parse import (
    parse_qs,
    urlencode,
    urljoin,
    urlparse,
)

import requests

CREATE_TASK_URL = "https://api.solvecaptcha.com/in.php"
GET_RESULT_URL = "https://api.solvecaptcha.com/res.php"

POLL_INTERVAL_SECONDS = 5
SOLVE_TIMEOUT_SECONDS = 180
COOKIE_FILE = Path("datadome-cookie.json")

DATADOME_IFRAME_PATTERN = re.compile(
    r"""
    <iframe
    [^>]+
    src=["']
    (
        [^"']*
        captcha-delivery\.com/captcha/
        [^"']*
    )
    ["']
    """,
    flags=re.IGNORECASE | re.VERBOSE,
)

class DataDomeError(RuntimeError):
    """Base error for the DataDome integration."""

class DataDomeExtractionError(DataDomeError):
    """Raised when challenge parameters cannot be extracted."""

class DataDomeProxyBlockedError(DataDomeError):
    """Raised when the challenge reports t=bv."""

class SolveCaptchaError(DataDomeError):
    """Raised when SolveCaptcha returns an API error."""

@dataclass(frozen=True)
class DataDomeCookie:
    value: str
    domain: str
    path: str
    secure: bool
    raw: str

def parse_api_response(
    response: requests.Response,
) -> dict[str, Any]:
    response.raise_for_status()

    try:
        payload = response.json()
    except requests.JSONDecodeError as exc:
        raise SolveCaptchaError(
            "SolveCaptcha returned invalid JSON: "
            f"{response.text[:500]}"
        ) from exc

    if not isinstance(payload, dict):
        raise SolveCaptchaError(
            f"Unexpected API response: {payload!r}"
        )

    return payload

def proxy_for_requests(
    proxy: str,
    proxy_type: str,
) -> str:
    normalized_type = proxy_type.lower()

    if normalized_type == "socks5":
        scheme = "socks5h"
    elif normalized_type == "socks4":
        scheme = "socks4a"
    elif normalized_type in {
        "http",
        "https",
    }:
        scheme = normalized_type
    else:
        raise ValueError(
            "Proxy type must be HTTP, HTTPS, "
            "SOCKS4, or SOCKS5"
        )

    return f"{scheme}://{proxy}"

def validate_captcha_url(
    captcha_url: str,
) -> None:
    parsed = urlparse(captcha_url)

    if (
        "captcha-delivery.com"
        not in parsed.hostname
        if parsed.hostname
        else True
    ):
        raise DataDomeExtractionError(
            "The URL is not a DataDome "
            "captcha-delivery URL"
        )

    query = parse_qs(parsed.query)
    challenge_type = query.get(
        "t",
        [None],
    )[0]

    if challenge_type == "bv":
        raise DataDomeProxyBlockedError(
            "DataDome reports t=bv. "
            "The proxy IP is blocked and must be replaced."
        )

    if (
        challenge_type is not None
        and challenge_type != "fe"
    ):
        raise DataDomeExtractionError(
            "Unsupported DataDome challenge type: "
            f"{challenge_type}"
        )

def extract_iframe_url(
    page_html: str,
    page_url: str,
) -> str | None:
    match = DATADOME_IFRAME_PATTERN.search(
        page_html
    )

    if not match:
        return None

    captcha_url = urljoin(
        page_url,
        html.unescape(
            match.group(1)
        ),
    )

    validate_captcha_url(
        captcha_url
    )

    return captcha_url

def extract_js_property(
    source: str,
    property_name: str,
) -> str | None:
    pattern = re.compile(
        rf"""
        ["']?
        {re.escape(property_name)}
        ["']?
        \s*:\s*
        ["']
        ([^"']+)
        ["']
        """,
        flags=re.IGNORECASE | re.VERBOSE,
    )

    match = pattern.search(source)

    if not match:
        return None

    return match.group(1)

def extract_dd_configuration(
    page_html: str,
) -> dict[str, str]:
    object_match = re.search(
        r"""
        (?:var\s+)?
        dd
        \s*=\s*
        \{
        (.*?)
        \}
        \s*;?
        """,
        page_html,
        flags=re.IGNORECASE
        | re.DOTALL
        | re.VERBOSE,
    )

    if not object_match:
        raise DataDomeExtractionError(
            "DataDome configuration object "
            "was not found"
        )

    source = object_match.group(1)

    result = {
        name: extract_js_property(
            source,
            name,
        )
        for name in (
            "cid",
            "hsh",
            "t",
            "s",
            "e",
        )
    }

    missing = [
        name
        for name, value in result.items()
        if not value
    ]

    if missing:
        raise DataDomeExtractionError(
            "Missing DataDome configuration: "
            + ", ".join(missing)
        )

    return {
        name: str(value)
        for name, value in result.items()
    }

def build_captcha_url(
    configuration: dict[str, str],
    temporary_cookie: str,
    page_url: str,
) -> str:
    challenge_type = configuration["t"]

    if challenge_type == "bv":
        raise DataDomeProxyBlockedError(
            "The current proxy IP is blocked "
            "by DataDome"
        )

    if challenge_type != "fe":
        raise DataDomeExtractionError(
            "Unsupported challenge type: "
            f"{challenge_type}"
        )

    query = urlencode(
        {
            "initialCid": configuration["cid"],
            "hash": configuration["hsh"],
            "cid": temporary_cookie,
            "t": challenge_type,
            "referer": page_url,
            "s": configuration["s"],
            "e": configuration["e"],
        }
    )

    captcha_url = (
        "https://geo.captcha-delivery.com/"
        f"captcha/?{query}"
    )

    validate_captcha_url(
        captcha_url
    )

    return captcha_url

def find_captcha_url(
    response: requests.Response,
) -> str:
    iframe_url = extract_iframe_url(
        page_html=response.text,
        page_url=response.url,
    )

    if iframe_url:
        return iframe_url

    temporary_cookie = response.cookies.get(
        "datadome"
    )

    if not temporary_cookie:
        raise DataDomeExtractionError(
            "The response contains neither "
            "a captcha iframe nor a temporary "
            "datadome cookie"
        )

    configuration = extract_dd_configuration(
        response.text
    )

    return build_captcha_url(
        configuration=configuration,
        temporary_cookie=temporary_cookie,
        page_url=response.url,
    )

def parse_datadome_cookie(
    raw_cookie: str,
    page_url: str,
) -> DataDomeCookie:
    cookie = SimpleCookie()

    try:
        cookie.load(raw_cookie)
    except Exception as exc:
        raise SolveCaptchaError(
            "Unable to parse the returned cookie"
        ) from exc

    morsel = cookie.get("datadome")

    if morsel is None:
        fallback = re.search(
            r"(?:^|;\s*)datadome=([^;]+)",
            raw_cookie,
            flags=re.IGNORECASE,
        )

        if not fallback:
            raise SolveCaptchaError(
                "SolveCaptcha response does not "
                "contain a datadome cookie"
            )

        value = fallback.group(1)
        domain = (
            urlparse(page_url).hostname
            or ""
        )
        path = "/"
        secure = page_url.startswith(
            "https://"
        )
    else:
        value = morsel.value
        domain = (
            morsel["domain"]
            or urlparse(page_url).hostname
            or ""
        )
        path = morsel["path"] or "/"
        secure = bool(
            morsel["secure"]
        )

    if not value:
        raise SolveCaptchaError(
            "The returned datadome cookie is empty"
        )

    return DataDomeCookie(
        value=value,
        domain=domain,
        path=path,
        secure=secure,
        raw=raw_cookie,
    )

class SolveCaptchaDataDomeClient:
    def __init__(
        self,
        api_key: str,
        request_timeout: float = 30,
    ) -> None:
        if not api_key:
            raise ValueError(
                "SolveCaptcha API key is required"
            )

        self.api_key = api_key
        self.request_timeout = request_timeout
        self.session = requests.Session()

    def create_task(
        self,
        *,
        captcha_url: str,
        page_url: str,
        user_agent: str,
        proxy: str,
        proxy_type: str,
    ) -> str:
        validate_captcha_url(
            captcha_url
        )

        payload = {
            "key": self.api_key,
            "method": "datadome",
            "captcha_url": captcha_url,
            "pageurl": page_url,
            "userAgent": user_agent,
            "proxy": proxy,
            "proxytype": proxy_type,
            "json": 1,
        }

        response = self.session.post(
            CREATE_TASK_URL,
            data=payload,
            timeout=self.request_timeout,
        )
        result = parse_api_response(
            response
        )

        if result.get("status") != 1:
            error_code = str(
                result.get(
                    "request",
                    "UNKNOWN_ERROR",
                )
            )

            raise SolveCaptchaError(
                "Unable to create DataDome task: "
                f"{error_code}"
            )

        task_id = str(
            result.get("request", "")
        ).strip()

        if not task_id:
            raise SolveCaptchaError(
                "SolveCaptcha did not return "
                "a task ID"
            )

        return task_id

    def get_result(
        self,
        task_id: str,
        page_url: str,
    ) -> DataDomeCookie | None:
        response = self.session.get(
            GET_RESULT_URL,
            params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
                "json": 1,
            },
            timeout=self.request_timeout,
        )
        result = parse_api_response(
            response
        )

        if result.get("status") == 1:
            raw_cookie = str(
                result.get("request", "")
            ).strip()

            return parse_datadome_cookie(
                raw_cookie=raw_cookie,
                page_url=page_url,
            )

        error_code = str(
            result.get(
                "request",
                "UNKNOWN_ERROR",
            )
        )

        if error_code == "CAPCHA_NOT_READY":
            return None

        raise SolveCaptchaError(
            f"DataDome task {task_id} failed: "
            f"{error_code}"
        )

    def solve(
        self,
        *,
        captcha_url: str,
        page_url: str,
        user_agent: str,
        proxy: str,
        proxy_type: str,
        max_wait: float = SOLVE_TIMEOUT_SECONDS,
    ) -> tuple[str, DataDomeCookie]:
        task_id = self.create_task(
            captcha_url=captcha_url,
            page_url=page_url,
            user_agent=user_agent,
            proxy=proxy,
            proxy_type=proxy_type,
        )

        deadline = time.monotonic() + max_wait

        while time.monotonic() < deadline:
            time.sleep(
                POLL_INTERVAL_SECONDS
            )

            cookie = self.get_result(
                task_id=task_id,
                page_url=page_url,
            )

            if cookie is not None:
                return task_id, cookie

        raise TimeoutError(
            f"SolveCaptcha task {task_id} "
            f"was not completed within "
            f"{max_wait:.0f} seconds"
        )

    def report(
        self,
        task_id: str,
        accepted: bool,
    ) -> None:
        response = self.session.get(
            GET_RESULT_URL,
            params={
                "key": self.api_key,
                "action": (
                    "reportgood"
                    if accepted
                    else "reportbad"
                ),
                "id": task_id,
                "json": 1,
            },
            timeout=self.request_timeout,
        )

        parse_api_response(
            response
        )

def save_cookie(
    cookie: DataDomeCookie,
) -> None:
    COOKIE_FILE.write_text(
        json.dumps(
            asdict(cookie),
            indent=2,
        ),
        encoding="utf-8",
    )

def load_cookie() -> DataDomeCookie | None:
    if not COOKIE_FILE.is_file():
        return None

    try:
        payload = json.loads(
            COOKIE_FILE.read_text(
                encoding="utf-8"
            )
        )

        return DataDomeCookie(
            value=str(payload["value"]),
            domain=str(payload["domain"]),
            path=str(
                payload.get(
                    "path",
                    "/",
                )
            ),
            secure=bool(
                payload.get(
                    "secure",
                    True,
                )
            ),
            raw=str(
                payload.get(
                    "raw",
                    "",
                )
            ),
        )
    except (
        KeyError,
        TypeError,
        ValueError,
        json.JSONDecodeError,
    ):
        return None

def apply_cookie(
    session: requests.Session,
    cookie: DataDomeCookie,
) -> None:
    session.cookies.set(
        "datadome",
        cookie.value,
        domain=cookie.domain,
        path=cookie.path,
        secure=cookie.secure,
    )

def main() -> None:
    api_key = os.environ.get(
        "SOLVECAPTCHA_API_KEY",
        "",
    ).strip()

    target_url = os.environ.get(
        "TARGET_URL",
        "",
    ).strip()

    proxy = os.environ.get(
        "TARGET_PROXY",
        "",
    ).strip()

    proxy_type = os.environ.get(
        "TARGET_PROXY_TYPE",
        "http",
    ).strip().lower()

    user_agent = os.environ.get(
        "BROWSER_USER_AGENT",
        "",
    ).strip()

    if not api_key:
        raise SystemExit(
            "Set SOLVECAPTCHA_API_KEY"
        )

    if not target_url:
        raise SystemExit(
            "Set TARGET_URL"
        )

    if not proxy:
        raise SystemExit(
            "Set TARGET_PROXY"
        )

    if not user_agent:
        raise SystemExit(
            "Set BROWSER_USER_AGENT"
        )

    proxy_url = proxy_for_requests(
        proxy=proxy,
        proxy_type=proxy_type,
    )

    session = requests.Session()

    session.proxies.update(
        {
            "http": proxy_url,
            "https": proxy_url,
        }
    )

    session.headers.update(
        {
            "User-Agent": user_agent,
            "Accept": (
                "text/html,application/xhtml+xml,"
                "application/xml;q=0.9,"
                "image/avif,image/webp,*/*;q=0.8"
            ),
            "Accept-Language": (
                "en-US,en;q=0.9"
            ),
        }
    )

    saved_cookie = load_cookie()

    if saved_cookie:
        apply_cookie(
            session,
            saved_cookie,
        )

    response = session.get(
        target_url,
        timeout=30,
        allow_redirects=True,
    )

    if response.status_code < 400:
        print(
            "The page is accessible. "
            "No new DataDome solution is required."
        )
        return

    if response.status_code != 403:
        raise RuntimeError(
            "The target returned an unexpected "
            f"HTTP status: {response.status_code}"
        )

    captcha_url = find_captcha_url(
        response
    )

    print(
        "DataDome captcha URL:",
        captcha_url,
    )

    client = SolveCaptchaDataDomeClient(
        api_key=api_key
    )

    task_id, cookie = client.solve(
        captcha_url=captcha_url,
        page_url=response.url,
        user_agent=user_agent,
        proxy=proxy,
        proxy_type=proxy_type,
    )

    apply_cookie(
        session,
        cookie,
    )

    save_cookie(
        cookie
    )

    retry_response = session.get(
        target_url,
        timeout=30,
        allow_redirects=True,
    )

    accepted = (
        retry_response.status_code < 400
    )

    if accepted:
        client.report(
            task_id=task_id,
            accepted=True,
        )

        print(
            "DataDome cookie was accepted."
        )
        print(
            "HTTP status:",
            retry_response.status_code,
        )
        return

    print(
        "The request is still blocked."
    )
    print(
        "HTTP status:",
        retry_response.status_code,
    )
    print(
        "Do not report the result as incorrect "
        "until proxy, User-Agent, challenge age, "
        "and request headers have been checked."
    )

if __name__ == "__main__":
    main()

Run the script:

python datadome_solver.py

A successful solution is saved to:

datadome-cookie.json

The file contains:

{
  "value": "H7LmQvR8kP3D...XyJcTt9LsAbQW2N7FZk",
  "domain": ".example.com",
  "path": "/",
  "secure": true,
  "raw": "datadome=H7LmQvR8kP3D...; Domain=.example.com; Path=/; Secure"
}

Minimal Python API example

The following version assumes that you already have the complete captcha URL:

import os
import time

import requests

API_KEY = os.environ["SOLVECAPTCHA_API_KEY"]

CAPTCHA_URL = (
    "https://geo.captcha-delivery.com/"
    "captcha/?initialCid=...&t=fe&referer=..."
)

PAGE_URL = (
    "https://example.com/protected-page"
)

USER_AGENT = os.environ[
    "BROWSER_USER_AGENT"
]

PROXY = os.environ[
    "TARGET_PROXY"
]

create_response = requests.post(
    "https://api.solvecaptcha.com/in.php",
    data={
        "key": API_KEY,
        "method": "datadome",
        "captcha_url": CAPTCHA_URL,
        "pageurl": PAGE_URL,
        "userAgent": USER_AGENT,
        "proxy": PROXY,
        "proxytype": "http",
        "json": 1,
    },
    timeout=30,
).json()

if create_response.get("status") != 1:
    raise RuntimeError(
        "Task creation failed: "
        f"{create_response.get('request')}"
    )

task_id = create_response["request"]

while True:
    time.sleep(5)

    result = requests.get(
        "https://api.solvecaptcha.com/res.php",
        params={
            "key": API_KEY,
            "action": "get",
            "id": task_id,
            "json": 1,
        },
        timeout=30,
    ).json()

    if result.get("status") == 1:
        datadome_cookie = result["request"]
        break

    if result.get("request") != "CAPCHA_NOT_READY":
        raise RuntimeError(
            "Task failed: "
            f"{result.get('request')}"
        )

print(datadome_cookie)

Apply the cookie to a Requests session

Suppose SolveCaptcha returns:

datadome=COOKIE_VALUE; Max-Age=31536000; Domain=.example.com; Path=/; Secure; SameSite=Lax

Parse it with SimpleCookie:

from http.cookies import SimpleCookie

import requests

raw_cookie = (
    "datadome=COOKIE_VALUE; "
    "Domain=.example.com; "
    "Path=/; Secure"
)

parsed = SimpleCookie()
parsed.load(raw_cookie)

morsel = parsed["datadome"]

session = requests.Session()

session.cookies.set(
    "datadome",
    morsel.value,
    domain=(
        morsel["domain"]
        or ".example.com"
    ),
    path=morsel["path"] or "/",
    secure=bool(morsel["secure"]),
)

Retry the website with:

  • the same proxy;
  • the same User-Agent;
  • the same headers;
  • the same session cookies.
response = session.get(
    "https://example.com/protected-page",
    timeout=30,
)

print(response.status_code)

Using the cookie in a raw Cookie header

When you do not use a cookie jar, extract only the first cookie pair:

cookie_header = (
    solution.set_cookie
    .split(";", 1)[0]
    .strip()
)

headers = {
    "User-Agent": user_agent,
    "Cookie": cookie_header,
}

Example value:

datadome=H7LmQvR8kP3D..XyJcTt9LsAbQW2N7FZk

Do not use the complete Set-Cookie string as a request header.

Apply the DataDome cookie in Selenium

For Selenium, open the target domain before adding the cookie.

from http.cookies import SimpleCookie
from urllib.parse import urlparse

from selenium import webdriver

target_url = (
    "https://example.com/protected-page"
)

raw_cookie = (
    "datadome=COOKIE_VALUE; "
    "Domain=.example.com; "
    "Path=/; Secure"
)

parsed = SimpleCookie()
parsed.load(raw_cookie)

morsel = parsed["datadome"]

driver = webdriver.Chrome()

try:
    driver.get(target_url)

    hostname = urlparse(
        target_url
    ).hostname

    driver.add_cookie(
        {
            "name": "datadome",
            "value": morsel.value,
            "domain": (
                morsel["domain"]
                or hostname
            ),
            "path": (
                morsel["path"]
                or "/"
            ),
            "secure": bool(
                morsel["secure"]
            ),
        }
    )

    driver.get(target_url)

finally:
    driver.quit()

The Selenium browser must use the same proxy and User-Agent supplied to SolveCaptcha.

Example Selenium proxy configuration depends on whether the proxy requires authentication. For authenticated proxies, a browser extension, Selenium Wire, or a local forwarding proxy may be required.

Headers must match the client

Copying every Chrome header into a Requests script does not automatically make Requests behave like Chrome.

Only send headers that are appropriate for the HTTP client and target request.

Common headers include:

headers = {
    "User-Agent": user_agent,
    "Accept": (
        "text/html,application/xhtml+xml,"
        "application/xml;q=0.9,*/*;q=0.8"
    ),
    "Accept-Language": "en-US,en;q=0.9",
    "Referer": page_url,
}

Do not blindly hardcode:

sec-ch-ua
sec-fetch-site
sec-fetch-mode
sec-fetch-dest

Their values depend on the real browser request context.

Inspect the target request through DevTools when reproducing a specific workflow.

The returned cookie may not be enough

A valid datadome cookie does not guarantee that every request will be accepted.

The website may also evaluate:

  • the proxy IP;
  • TLS fingerprint;
  • HTTP version;
  • browser JavaScript behavior;
  • other session cookies;
  • navigation sequence;
  • request frequency;
  • header consistency;
  • application-specific authentication;
  • API request signatures.

If a Requests client remains blocked while the same cookie works in a browser, the difference may be caused by the browser fingerprint rather than the captcha result.

For browser-dependent websites, apply the cookie inside Selenium, Playwright, Puppeteer, or a remote browser session.

Detect whether the response is a DataDome block

An HTTP 403 response alone does not prove that DataDome caused the block.

Check for indicators such as:

captcha-delivery.com
datadome
initialCid
var dd
geo.captcha-delivery.com/captcha/

Example:

def is_datadome_response(
    response: requests.Response,
) -> bool:
    source = response.text.lower()

    indicators = (
        "captcha-delivery.com",
        "initialcid",
        "var dd",
        "datadome",
    )

    return any(
        indicator in source
        for indicator in indicators
    )

Use it before creating a SolveCaptcha task:

if (
    response.status_code == 403
    and is_datadome_response(response)
):
    print("DataDome challenge detected")

A 403 response can also be caused by:

  • access-control rules;
  • authentication failure;
  • geographic restrictions;
  • IP allowlists;
  • rate limits;
  • custom application logic.

Cookie expiration

Do not assume that every datadome cookie lasts exactly 15 or 30 minutes.

The lifetime depends on the protected website and the attributes returned with the cookie.

Inspect:

Max-Age
Expires

Even when the cookie has a long formal lifetime, DataDome can challenge the session again because of:

  • an IP change;
  • a User-Agent change;
  • inconsistent browser behavior;
  • another security rule;
  • excessive request volume;
  • a new risk decision.

Refresh the cookie when the website presents a new challenge.

Troubleshooting

ERR_PROXY_CONNECTION_FAILED

This error indicates that SolveCaptcha could not connect through the submitted proxy.

Check:

  • proxy hostname;
  • port;
  • username;
  • password;
  • proxy type;
  • firewall restrictions;
  • whether the proxy accepts external connections;
  • whether the proxy supports HTTPS targets.

Test the proxy separately:

proxy_url = (
    "http://login:password@"
    "proxy.example.com:8000"
)

response = requests.get(
    "https://api.ipify.org?format=json",
    proxies={
        "http": proxy_url,
        "https": proxy_url,
    },
    timeout=20,
)

print(response.json())

Do not continuously resubmit the same task with a nonworking proxy.

ERROR_CAPTCHA_UNSOLVABLE

For DataDome, this error may indicate:

  • blocked proxy;
  • expired captcha URL;
  • unsupported challenge state;
  • mismatched User-Agent;
  • proxy connection failure;
  • stale session values;
  • a captcha URL containing t=bv.

Recommended action:

  1. Check the t parameter.
  2. Test the proxy.
  3. Replace the proxy if necessary.
  4. Request the protected page again.
  5. Extract a new captcha URL.
  6. Create a new SolveCaptcha task.
  7. Limit the total number of retries.

Do not reuse an old captcha URL after changing the proxy.

CAPCHA_NOT_READY

This response means the task is still being processed:

{
  "status": 0,
  "request": "CAPCHA_NOT_READY"
}

Correct behavior:

  1. Keep the same task ID.
  2. Wait five seconds.
  3. Request the result again.
  4. Stop after a defined timeout.

Do not create another task for the same captcha while the original task is active.

ERROR_WRONG_USER_KEY

The SolveCaptcha API key is invalid.

Check:

SOLVECAPTCHA_API_KEY

Stop submitting tasks until the value is corrected.

ERROR_ZERO_BALANCE

The SolveCaptcha account does not have enough balance.

Stop task creation until the balance is replenished.

The iframe URL cannot be found

The challenge may be returned:

  • in the blocked response body;
  • after a redirect;
  • inside dynamically generated JavaScript;
  • only after a protected action;
  • inside another frame.

Enable Preserve log and inspect the complete request sequence.

The cookie works once and then fails

The website may have generated a new risk decision or invalidated the session.

Preserve the browser context and avoid changing:

  • IP address;
  • User-Agent;
  • cookies;
  • navigation order.

When a new challenge appears, repeat the complete solving workflow.

Persisting the DataDome cookie

A cookie can be stored between script runs, but it should not be assumed to remain valid indefinitely.

Store at least:

{
  "value": "COOKIE_VALUE",
  "domain": ".example.com",
  "path": "/",
  "secure": true
}

Before reusing the cookie:

  1. Use the same proxy or sticky proxy session.
  2. Use the same User-Agent.
  3. Request the target page.
  4. Detect whether DataDome accepts the cookie.
  5. Solve a new challenge if the cookie has expired or been invalidated.

Do not assume that the Max-Age attribute guarantees acceptance for that full period.

Reporting accepted and rejected results

When the returned cookie is accepted, report:

action=reportgood

Example:

https://api.solvecaptcha.com/res.php?key=YOUR_API_KEY&action=reportgood&id=TASK_ID

Use:

action=reportbad

only when you have confirmed that the returned captcha solution itself was incorrect.

Do not send reportbad when the failure was caused by:

  • a changed proxy;
  • a mismatched User-Agent;
  • an expired captcha URL;
  • incorrect cookie-domain handling;
  • missing browser cookies;
  • request-header differences;
  • another DataDome block after excessive traffic;
  • an application-level 403 response;
  • a TLS or browser-fingerprint mismatch.

Common implementation mistakes

Using createTask and getTaskResult

Incorrect:

createTask
getTaskResult
DataDomeSliderTask
clientKey

Correct:

in.php
res.php
method=datadome
key

Using captchaUrl instead of captcha_url

Incorrect:

captchaUrl=URL

Correct:

captcha_url=URL

Splitting the proxy into multiple parameters

Incorrect:

proxyAddress
proxyPort
proxyLogin
proxyPassword

Correct:

proxy=login:password@host:port
proxytype=HTTP

Using no proxy

DataDome tasks require a proxy.

Sending only the proxy to Requests

The proxy must also be included in the SolveCaptcha task.

Using different proxy sessions

Use the same IP throughout the complete challenge workflow.

Ignoring t=bv

t=bv means the proxy IP is blocked. Replace the proxy before creating another task.

Replacing t=bv with t=fe manually

Do not modify the query parameter. A blocked proxy remains blocked even if the URL text is changed.

Sending the wrong User-Agent

The value must match the client that received the challenge.

Rebuilding an iframe URL unnecessarily

Use the complete iframe src whenever it is available.

Losing URL encoding

The captcha URL contains encoded values. Send it as a normal form field and allow the HTTP library to encode it.

Extracting the wrong cid

The fallback URL can use both:

  • initialCid from the inline DataDome object;
  • cid from the temporary datadome cookie.

They are not necessarily identical.

Saving only part of the response incorrectly

The API returns a complete cookie string. Parse the datadome value and preserve the relevant attributes.

Sending cookie attributes in the Cookie header

Send only:

datadome=COOKIE_VALUE

Do not send Max-Age, Domain, Path, Secure, or SameSite as request cookies.

Applying the cookie to the wrong domain

Use the Domain attribute returned with the cookie or the hostname of the protected website.

Assuming a fixed cookie lifetime

Use the returned expiration attributes and detect new challenge responses instead of assuming a universal lifetime.

Assuming every 403 is DataDome

Confirm the presence of DataDome indicators before creating a task.

Stability recommendations

Keep one session consistent

Preserve:

Proxy
User-Agent
Cookies
Page URL
Browser profile

until the protected workflow is complete.

Validate before creating a task

Check:

  • captcha URL hostname;
  • t parameter;
  • proxy connectivity;
  • User-Agent value;
  • page URL;
  • API key.

Use controlled retry limits

Do not retry indefinitely.

A practical policy is:

Task polling timeout: 180 seconds
Polling interval: 5 seconds
Proxy replacement: after confirmed proxy failure
New task: only after obtaining a fresh captcha URL

Log useful diagnostic data

Record:

  • task ID;
  • creation time;
  • result time;
  • page URL;
  • captcha hostname;
  • proxy hostname;
  • proxy type;
  • User-Agent;
  • API error code;
  • final HTTP status.

Do not log:

  • API keys;
  • proxy passwords;
  • full authentication cookies;
  • sensitive account data.

Do not rotate blindly

Replace the proxy only when:

  • the URL contains t=bv;
  • proxy connection fails;
  • the proxy is confirmed blocked;
  • the network session is no longer usable.

Every proxy change requires a new page load and a new captcha URL.

Recommended project structure

Separate the workflow into independent components:

datadome_solver/
├── detector.py
├── extractor.py
├── solvecaptcha.py
├── cookies.py
├── browser.py
├── settings.py
└── main.py

Suggested responsibilities:

detector.py
    Determines whether a response contains a
    DataDome challenge.

extractor.py
    Extracts or builds the captcha URL.

solvecaptcha.py
    Creates tasks and polls the API.

cookies.py
    Parses, stores, and applies datadome cookies.

browser.py
    Manages browser sessions, proxies, and
    User-Agent values.

settings.py
    Loads API credentials and configuration.

main.py
    Coordinates the complete solving workflow.

This structure makes it easier to replace Requests with Selenium, Playwright, Puppeteer, or a remote browser without rewriting the SolveCaptcha client.

Pre-launch checklist

Before deploying the integration, confirm:

  • [ ] The page is protected by DataDome.
  • [ ] The complete captcha iframe URL was captured.
  • [ ] The captcha URL belongs to the current page load.
  • [ ] The t parameter is fe when present.
  • [ ] The URL does not contain t=bv.
  • [ ] The page URL is complete and correct.
  • [ ] The User-Agent matches the original browser.
  • [ ] A working proxy is configured.
  • [ ] The same proxy is used after solving.
  • [ ] The proxy format is login:password@host:port.
  • [ ] method=datadome is used.
  • [ ] The task ID is read from request.
  • [ ] Polling continues every five seconds.
  • [ ] The returned cookie string is parsed correctly.
  • [ ] Only the datadome pair is sent in a raw Cookie header.
  • [ ] A fresh task is created when a new challenge appears.

Frequently asked questions

Which SolveCaptcha method is used for DataDome?

Use:

method=datadome

Is a proxy required?

Yes. The SolveCaptcha DataDome method requires:

proxy
proxytype

Which proxy types are supported?

Supported values are:

HTTP
HTTPS
SOCKS4
SOCKS5

Where can I find captcha_url?

Use the complete src value of the iframe containing the DataDome captcha.

What does t=fe mean?

t=fe identifies a captcha challenge that can be submitted for solving.

What does t=bv mean?

t=bv indicates that the proxy IP is blocked by DataDome. Change the proxy and obtain a new captcha URL.

What does SolveCaptcha return?

It returns a complete datadome cookie string:

datadome=VALUE; Domain=.example.com; Path=/; Secure

How often should the result be checked?

Wait five seconds between requests while the API returns:

CAPCHA_NOT_READY

Can the cookie be used with another proxy?

No. Use the cookie with the same proxy session used for the challenge.

Can the cookie be used with another User-Agent?

Do not change the User-Agent after solving the challenge.

Can I reuse a captcha URL?

No. Treat the captcha URL as short-lived and session-specific.

Why does the cookie work in Selenium but not Requests?

The website may evaluate browser JavaScript, TLS characteristics, HTTP headers, navigation state, and other signals that Requests does not reproduce.

Why am I still receiving HTTP 403?

Possible reasons include:

  • blocked proxy;
  • stale captcha URL;
  • incorrect cookie domain;
  • changed User-Agent;
  • changed proxy IP;
  • another bot-protection rule;
  • unsupported HTTP fingerprint;
  • missing session cookies;
  • excessive request frequency.

Must the proxy be residential?

The API specification does not require a particular commercial proxy category.

The proxy must be accessible, stable, and not blocked by DataDome.

Can I solve DataDome without a browser?

The challenge URL may be extracted from an HTTP response, but the complete flow still requires an accurate page URL, User-Agent, working proxy, and correct cookie handling.

Which programming languages are supported?

Any language capable of sending HTTP requests can use the API, including:

  • Python;
  • JavaScript;
  • PHP;
  • Java;
  • C#;
  • Go;
  • Ruby.

Conclusion

A reliable DataDome bypass workflow must preserve the complete challenge context.

The correct process is:

  1. send the target request through a stable proxy;
  2. confirm that the response contains DataDome;
  3. extract the complete captcha iframe URL;
  4. verify that the URL does not contain t=bv;
  5. submit the task with method=datadome;
  6. provide the same proxy and User-Agent;
  7. poll the result every five seconds;
  8. parse the complete returned cookie;
  9. apply it to the original session;
  10. retry the target request without changing the proxy;
  11. create a new task when the challenge expires;
  12. report accepted or genuinely incorrect answers accurately.

Use the SolveCaptcha DataDome API to integrate DataDome captcha handling into authorized Python scraping, browser automation, and QA projects.