DataDome
Create a task through sync api https://sync.ez-captcha.com/createSyncTask, and get the result directly
If you obtain an invalid token, please contact us. It will usually work normally after we optimize it.
Task Type
Task Type | Description | Price(USD) |
|---|---|---|
DataDomeTaskProxyless | DataDome solution | step1: $0.1/k step2: $2.5/k |
Support Options
Type | Support Status |
|---|---|
Slider | ✔ support |
Interstitial | ✔ support |
Parameter Structure
Parameter | Type | Required | Description |
|---|---|---|---|
clientKey | string | true | Your client key |
type | string | true | DataDomeTaskProxyless |
step | string | true | 1 = Get the challenge URL; 2 = Get Release Results |
referer | string | true | In Step 1, enter the page URL; in Step 2, enter the URL returned by Step 1 |
html_b64 | string | true | Base64-encoded HTML text |
user_agent | string | false | Step 1 is optional, Step 2 is required. Currently, only the versions listed below are supported:
|
Important Notes:
Currently, the only supported languages are:
en-US;q=0.9
Usage Instructions
The EZ DataDome processing workflow consists of two steps:
Step 1:Submit the initial page (Get the challenge)
Interface
POST https://sync.ez-captcha.com/createSyncTaskRequest Parameters
{
"clientKey": "your_client_key",
"task": {
"type": "DataDomeTaskProxyless",
"step": "1", # 1 = Get the challenge URL
"html_b64": "<HTML text in Base64>",
"referer": "https://target.com/page"
}
}Parameter Description
Parameter | Description |
|---|---|
step=1 | Get the DataDome challenge URL |
html_b64 | Page HTML (must be Base64-encoded) |
referer | URL of the current page |
Return results
{
"errorId": 0,
"solution": {url: "https://geo.captcha-delivery.com/captcha/xxx"}
}Step 2:Submit Challenge Page (Get Solution)
Request Parameters
{
"clientKey": "your_client_key",
"task": {
"type": "DataDomeTaskProxyless",
"step": "2",
"html_b64": "<Base64 Challenge Page HTML>",
"referer": "https://geo.captcha-delivery.com/captcha/xxx",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36"
}
}Return Results
{
"errorId": 0,
"solution": {
"kind": "slider", // Specify whether the trigger is a Slider or an Interstitial
"url": "https://geo.captcha-delivery.com/validate/xxx",
"body": null
}
}Step 3:Submit verification results
Execute different requests based on the returned `kind`:
1. slider type
GET {solution.url}2. interstitial type
POST {solution.url}
Content-Type: application/x-www-form-urlencoded
{solution.body}Return Results
{
"errorId": 0,
"view": "redirect",
"cookie": "datadome=xxxxx"
}Instructions for Using Cookies
After the API request is processed successfully, it will return:
datadome=xxxxxxxxxxxxDirections for use:
Write to the current session
Used for authenticating subsequent requests
Demo
# -*- coding: utf-8 -*-
"""
DataDome solver service - slider integration example
Requirements:
pip install requests
Usage:
1) Configure API_URL, CLIENT_KEY, and USER_AGENT below.
2) Configure a residential proxy in PROXY. Slider challenges require a clean
residential IP, and the same outbound IP must be used for the entire flow.
POST {API_URL}
JSON body:
{
"clientKey": "<your API key>",
"task": {
"type": "DataDomeTaskProxyless",
"step": "1" or "2", # 1 = get challenge URL; 2 = get pass result
"html_b64": "<base64-encoded HTML>",
"referer": "<page URL for step1; step1 URL for step2>",
"user_agent": "<fixed user agent specified by EZ>"
}
}
A successful JSON response has errorId=0 and the result in solution.
A failed response has errorId!=0; see errorCode and errorDescription.
The step1 solution is {"url": challenge URL}.
For a slider, the step2 solution is {"url": verification URL, "body": null}.
Always use the locally configured USER_AGENT.
Important requirements:
- user_agent must be the fixed value provided by EZ. Do not modify or rotate it.
- Encode all HTML as base64 before assigning it to html_b64.
- Use one session, preserve site cookies, and keep the same outbound IP for
the entire flow.
"""
import base64
import json
import re
import time
import random
import requests
def generate_random_number(min_length=8, max_length=13):
length = random.randint(min_length, max_length)
first_digit = random.randint(1, 9)
remaining_digits = [random.randint(0, 9) for _ in range(length - 1)]
number = int(''.join(map(str, [first_digit] + remaining_digits)))
return number
class Banned(Exception):
"""The current outbound IP or session is banned by DataDome."""
# ============================ Configuration ============================
# Solver service endpoint.
API_URL = "https://sync.ez-captcha.com/createSyncTask"
# Your API key.
CLIENT_KEY = "11b6106e35e0476890a95291a543657658"
# Fixed user agent.
USER_AGENT = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36"
)
# Residential proxy required by slider sites. Example: "http://user:pass@host:port"
PROXY = "" + str(
generate_random_number(8)) + ""
# A page on the target site that triggers DataDome.
TARGET_URL = "https://www.xxx.com/xxx/xxx"
# Delay before verification to simulate human solving time.
SOLVE_DELAY = 3
# ======================================================================
def _b64(text: str) -> str:
"""Encode HTML text as base64."""
return base64.b64encode(text.encode("utf-8")).decode("ascii")
def _is_blocked(html: str) -> bool:
"""Return whether the HTML is a DataDome hard-block page."""
return "hardblock" in html
def _sec_ch_ua(ua: str) -> str:
"""Build a sec-ch-ua value whose version matches the fixed user agent."""
m = re.search(r"Chrome/(\d+)", ua)
v = m.group(1) if m else "148"
return f'"Chromium";v="{v}", "Google Chrome";v="{v}", "Not/A)Brand";v="99"'
def call_solver(step: int, html: str, referer: str) -> object:
"""
Call the solver service.
step: 1 gets the challenge URL; 2 gets the pass result.
html: Raw HTML for the current page or challenge page. This function encodes it.
referer: Page URL for step1; the URL returned by step1 for step2.
Returns:
step1 -> dict: {"url": challenge URL}
step2 -> dict: {"url", "body", "kind"}; slider uses GET, and
interstitial uses POST.
"""
payload = {
"clientKey": CLIENT_KEY,
"task": {
"type": "DataDomeTaskProxyless",
"step": str(step),
"html_b64": _b64(html),
"referer": referer,
"user_agent": USER_AGENT,
},
}
resp = requests.post(API_URL, json=payload, timeout=60)
data = resp.json()
print(f"step{step} solver response: {data}")
resp.raise_for_status()
# errorId != 0 indicates failure; details are in errorCode/errorDescription.
if data.get("errorId", 0) != 0:
code = data.get("errorCode", "")
desc = data.get("errorDescription", "")
# "blocked" means DataDome banned the IP/session. Retry with a new IP.
if "blocked" in f"{code} {desc}".lower():
raise Banned(desc or code or "blocked")
raise RuntimeError(f"Solver failed: {code} {desc}".strip())
solution = data.get("solution")
# Some gateways return solution as a JSON string; normalize it here.
if isinstance(solution, str):
try:
solution = json.loads(solution)
except json.JSONDecodeError:
pass
return solution
def _nav_headers(fetch_site: str) -> dict:
"""Build navigation headers. Use none initially and same-origin to verify."""
return {
"User-Agent": USER_AGENT,
"Accept": ("text/html,application/xhtml+xml,application/xml;q=0.9,"
"image/avif,image/webp,image/apng,*/*;q=0.8"),
"Accept-Language": "en-US;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"sec-ch-ua": _sec_ch_ua(USER_AGENT),
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Site": fetch_site,
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-User": "?1",
"Sec-Fetch-Dest": "document",
}
def _challenge_headers(referer: str) -> dict:
"""Build headers for loading the cross-site slider iframe."""
return {
"User-Agent": USER_AGENT,
"Accept": ("text/html,application/xhtml+xml,application/xml;q=0.9,"
"image/avif,image/webp,image/apng,*/*;q=0.8"),
"Accept-Language": "en-US;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"sec-ch-ua": _sec_ch_ua(USER_AGENT),
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Dest": "iframe",
"Referer": referer,
}
def _check_headers(referer: str, ua: str) -> dict:
"""Build same-origin CORS headers for fetching /captcha/check."""
return {
"User-Agent": ua,
"Accept": "*/*",
"Accept-Language": "en-US;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"sec-ch-ua": _sec_ch_ua(ua),
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
"Referer": referer,
}
def _post_headers(referer: str, ua: str) -> dict:
"""Build headers for posting an interstitial payload to DataDome."""
return {
"User-Agent": ua,
"Accept": "*/*",
"Accept-Language": "en-US;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Origin": "https://geo.captcha-delivery.com",
"Referer": referer,
"sec-ch-ua": _sec_ch_ua(ua),
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
}
def _submit(session, sol: dict, challenge_url: str, used_ua: str):
"""Submit the solution to DataDome and return its cookie string.
A slider uses GET; an interstitial posts the form body. If kind is absent,
an empty body is treated as a slider according to the service contract.
Return None when verification does not pass.
"""
kind = (sol.get("kind") or "").strip().lower()
body = sol.get("body")
submit_url = sol["url"]
is_slider = (kind == "slider") if kind else (not body)
if is_slider:
res = session.get(
submit_url, headers=_check_headers(challenge_url, used_ua), timeout=30
).json()
else:
res = session.post(
submit_url, headers=_post_headers(challenge_url, used_ua),
data=(body or "").encode("utf-8"), timeout=30,
).json()
print(f"Verification response: {res}")
# An interstitial passes with view=redirect. Both flows return a cookie.
if not is_slider and res.get("view") != "redirect":
print(f"Not passed: view={res.get('view')}. Retry with a new proxy/IP.")
return None
cookie_field = res.get("cookie", "")
if not cookie_field:
print(f"Not passed: no cookie returned: {res}. Retry with a new proxy/IP.")
return None
return cookie_field
def main() -> None:
session = requests.Session()
if PROXY:
session.proxies = {"http": PROXY, "https": PROXY}
try:
# 1) Fetch the target page. Expect HTTP 403 with a challenge script.
print("[1/5] Fetching target page ...")
html = session.get(TARGET_URL, headers=_nav_headers("none"), timeout=30).text
if _is_blocked(html):
raise Banned("The initial page is a hard-block page")
if "var dd" not in html:
print("DataDome was not triggered; the request may already be allowed.")
return
# 2) Step 1: get the slider challenge URL.
print("[2/5] Getting challenge URL ...")
step1_solution = call_solver(1, html, TARGET_URL)
challenge_url = (
step1_solution.get("url")
if isinstance(step1_solution, dict)
else step1_solution
)
if not isinstance(challenge_url, str) or not challenge_url:
raise RuntimeError(f"Unexpected step1 response: {step1_solution!r}")
# 3) Fetch the slider challenge page.
print("[3/5] Fetching challenge page ...")
challenge_html = session.get(
challenge_url, headers=_challenge_headers(TARGET_URL), timeout=30
).text
if _is_blocked(challenge_html):
raise Banned("The challenge page is a hard-block page")
# 4) Step 2: solve and get the submission URL/body.
print("[4/5] Solving challenge ...")
sol = call_solver(2, challenge_html, challenge_url)
used_ua = USER_AGENT # Keep the fixed local UA; ignore server-side UA values.
# Wait before submission to simulate human solving time.
time.sleep(SOLVE_DELAY)
# 5) Submit by kind (slider GET or interstitial POST) and get the cookie.
print("[5/5] Submitting solution ...")
cookie_field = _submit(session, sol, challenge_url, used_ua)
if not cookie_field:
return
print("Passed; received the datadome cookie")
# 6) Verify access with the new datadome cookie.
# Remove the old challenge cookie from the initial 403 response first;
# otherwise it can conflict with the new cookie and cause another 403.
# Keep all other site cookies stored by Session.
new_dd = cookie_field.split(";")[0].split("=", 1)[-1]
for c in list(session.cookies):
if c.name == "datadome":
session.cookies.clear(c.domain, c.path, c.name)
session.cookies.set("datadome", new_dd, domain=".xxx.com", path="/")
v = session.get(TARGET_URL, headers=_nav_headers("same-origin"), timeout=30)
ok = v.status_code in (200, 302) and "var dd" not in v.text and not _is_blocked(v.text)
status = "allowed" if ok else "still blocked"
print(f"Target verification: HTTP {v.status_code}, {status}")
except Banned as e:
# Retry the full flow with a clean residential proxy/IP after a ban.
print(f"Banned: {e}. Retry with a different residential proxy/IP.")
if __name__ == "__main__":
main()