🎉 Premium Proxies · 3-Day Free TrialClaim Now →
Guides

How to Bypass DataDome When Web Scraping

Bypass DataDome when scraping public data: how it fingerprints devices, TLS, and behavior, plus real browsers, residential IPs, pacing, and working API code.

S SparkProxy 2 18 min read
Share
How to Bypass DataDome When Web Scraping

Trying to bypass DataDome with a plain requests.get() returns a 403 on the first call, every time. DataDome is one of the strongest bot-detection systems on the web, and it decides whether your traffic looks like a real user before your code ever sees the HTML. This guide explains exactly how DataDome fingerprints a request, how to read its block responses, and the practical, ethical ways to collect public data from DataDome-protected sites without pretending your traffic is something it isn't. There's no magic bullet and no permanent bypass. What follows are the signals that actually decide the outcome, and how to make legitimate automated requests look like the real browser sessions they are.

Scrape responsibly: what "bypass" really means

"Bypass" here means one thing: making legitimate, automated access to public data look like the ordinary browser traffic it already is, so a heuristic doesn't wrongly flag it. It does not mean breaking into anything protected. Before you write a line of code, set the ground rules.

  • Public data only. Anything behind a login, a paywall, or that exposes someone's personal information is off-limits without explicit permission. DataDome protects fraud-sensitive flows too (checkout, account creation, gift cards). Don't touch those.
  • Read robots.txt and the Terms of Service. If a path is disallowed or the ToS prohibits automated collection, respect it. A Crawl-delay directive is a rate the site is asking you to honor.
  • Rate-limit yourself. DataDome exists in part because scrapers hammer origins. Slow, considerate crawling is both more ethical and, conveniently, far less detectable.
  • Prefer the official API. If the site publishes a data feed or API, use it. It's faster, cleaner, and it's the access path the site actually sanctions.
  • No guarantees. DataDome ships detection updates continuously. A technique that works today can stop working next week. Anyone selling a "permanent DataDome bypass" is selling snake oil.

The goal is not to defeat security. It's to stop a bot filter from misclassifying a well-behaved, public-data crawler as an attacker. If a site clearly does not want to be scraped, the correct answer is to stop, not to escalate.

This guide stays on the right side of that line. Everything below is about fingerprint realism and good manners, not about defeating authentication or scraping data that was never meant to be public.


How DataDome detects bots

DataDome runs as a server-side module plus an edge layer and a client-side JavaScript tag. Every request is scored across several independent layers at once, and a single strong mismatch is enough to trip a block. This is why swapping in a "better proxy" rarely fixes anything on its own: the IP is only one row in the table.

Detection layerWhat DataDome inspectsWhat gives a bot away
IP reputation and ASNAddress history, whether the ASN is a known datacenter (AWS, GCP, Azure, OVH)A clean-looking request from a flagged datacenter range
TLS fingerprint (JA3/JA4)Cipher suites, extensions, curves in the TLS Client HelloPython `urllib3` or Go defaults that match no real browser
HTTP/2 fingerprintFrame settings, header order, pseudo-header order, priorityA `Chrome` User-Agent over an HTTP/2 profile no Chrome would send
HTTP headers and client hints`User-Agent`, `Accept-*`, `sec-ch-ua`, header order and casingMissing client hints, wrong `Accept`, alphabetized headers
JavaScript environment`navigator.webdriver`, canvas, WebGL, audio, fonts, screen, timezoneHeadless flags, `SwiftShader` WebGL, automation properties
BehaviorMouse and touch events, request cadence, navigation orderZero mouse movement, requests with no homepage visit first

The important mental model: DataDome cross-checks these layers against each other. A Chrome User-Agent paired with a Python TLS handshake is more suspicious than an honest Python client, because the inconsistency is the tell. To pass, every layer has to agree on the same story. That's the core principle behind everything in the proxy-block avoidance playbook, and it applies double to DataDome.


Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

Decode the response: 403 vs 429 vs challenge

Most scrapers treat "not 200" as one failure. DataDome actually gives you a diagnostic if you read the status code plus the embedded dd object. Different outcomes need different fixes, and retrying the wrong way just burns IPs.

ResponseWhat you'll seeWhat DataDome decidedCorrect action
**200 (real content)**Your HTML/JSON, plus a set or refreshed `datadome` cookiePassed every layerPersist the cookie, keep the same exit IP for this session
**200 (interstitial)**A short page with a `dd` object and a `js.datadome.co` script, not your contentSilent JS challenge issuedRender it in a real browser so the JS runs and refreshes the cookie
**403 (`rt:"b"`)**Block page, `X-DataDome` header, `dd` object with `"rt":"b"`Hard block: reputation or fingerprint failed outrightRotate IP and fix the fingerprint; do not retry the identical request
**403 (`rt:"c"`)**Block page with `"rt":"c"` and a `captcha-delivery.com` CAPTCHAChallenge: prove a real deviceSolve inside a genuine browser session, or route through a rendering API
**429**A rate-limit response, often with `Retry-After`Too many requests from this IP or identifierBack off, drop concurrency, slow the crawl
**530**Returned by a scraping API when all upstream attempts failThe proxy or render layer exhausted its retriesRetry with backoff; escalate to premium proxy plus stealth

The practical takeaway: a 403 with "rt":"c" is not the same problem as a 403 with "rt":"b". The first wants a real browser to answer a challenge. The second wants a cleaner IP and a more honest fingerprint. A 429 wants patience, not a new proxy. Branch on that instead of blindly rotating.


Use a real browser, not a bare HTTP client

DataDome's JavaScript fingerprint is the layer a plain HTTP library cannot satisfy, because there's no JS runtime to interrogate. The tag checks navigator.webdriver, canvas and WebGL output, the audio stack, installed fonts, screen geometry, and dozens of automation tells. Satisfying all of that reliably means running an actual browser.

For headless automation, launch Chromium through Playwright or Puppeteer and patch the obvious automation leaks with a stealth layer. Here's Playwright in Python routing through a residential proxy, with the stealth patch applied before the first navigation:

from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync

with sync_playwright() as p:
    browser = p.chromium.launch(
        headless=True,
        proxy={"server": "http://gate.sparkproxy.io:10000",
               "username": "user", "password": "pass"},
    )
    context = browser.new_context(
        viewport={"width": 1280, "height": 800},
        locale="en-US",
        timezone_id="America/New_York",   # match the proxy's country
    )
    page = context.new_page()
    stealth_sync(page)                    # hides navigator.webdriver and headless tells

    page.goto("https://example.com/", wait_until="networkidle")   # earn a datadome cookie
    page.goto("https://example.com/listings", wait_until="networkidle")
    html = page.content()
    browser.close()

Two details matter. The first goto hits the homepage so the DataDome tag can set a legitimate datadome cookie before you request the page you actually want, exactly as a human's browser would. And the viewport, locale, and timezone are set to consistent, human values rather than the headless defaults (800x600, UTC) that DataDome scores as robotic. If you're using Puppeteer instead, the same proxy-and-stealth pattern applies; see the guide to using proxies with Puppeteer for the Node setup.

A real browser is heavier than an HTTP client. Run one only for targets that genuinely challenge you, and reserve lightweight clients for everything else.


Match your TLS/JA3 fingerprint

When you can avoid a full browser (some DataDome-protected endpoints only score the network and header layers), the request still has to survive TLS fingerprinting. Python's requests rides on urllib3, whose TLS Client Hello matches no browser on earth. DataDome computes a JA3/JA4 hash from that handshake and sees "Python," no matter what User-Agent you set.

The fix is curl_cffi, which impersonates a real browser's TLS stack and HTTP/2 profile:

from curl_cffi import requests as cffi

session = cffi.Session(impersonate="chrome124")   # real Chrome TLS + HTTP/2
session.proxies = {
    "http":  "http://user:pass@gate.sparkproxy.io:10000",
    "https": "http://user:pass@gate.sparkproxy.io:10000",
}

resp = session.get("https://example.com/listings")
print(resp.status_code)          # 200 if the fingerprint and IP hold up

impersonate="chrome124" sends Chrome 124's exact cipher suites, extensions, and HTTP/2 settings, so the JA3/JA4 hash and the User-Agent finally tell the same story. That consistency is the whole point. For the broader picture on TLS realism and why a matched fingerprint beats a fancier proxy, the proxy-block avoidance guide goes layer by layer, and the async scraping with requests and aiohttp walkthrough covers doing this at volume.

Be honest with yourself about which path a target needs. If DataDome serves a JS challenge (rt:"c"), no TLS trick alone will clear it. That's a job for a real browser or a rendering API.


Use residential or mobile IPs with clean reputation

IP reputation is the fastest way to get an instant 403 from DataDome. Requests from well-known datacenter ASNs (AWS, GCP, Azure, OVH, Hetzner) start with a heavy suspicion penalty, because almost no ordinary shopper browses from an AWS IP. You can have a perfect browser fingerprint and still get blocked purely on the exit address.

Residential and mobile proxies route through real consumer ISPs and carrier networks, so the exit IP carries the reputation of an ordinary home or phone connection. That single change often turns a reliable 403 into a 200. If you're new to the differences between IP families, the residential proxy explainer breaks down when each type fits.

Two rules keep a good IP good:

  • One session, one IP. Bind a datadome cookie to the exit IP that earned it. Don't replay the same cookie across a rotating pool; that identifier-to-IP mismatch is exactly what DataDome flags.
  • Geo-match the audience. If you're scraping a US retailer, exit from a US residential IP. A German IP hitting a US-only storefront is an easy anomaly.

Residential IPs are not a standalone bypass. They clear the reputation layer, but the fingerprint and behavior layers still have to pass. Pair a clean IP with a real browser or a matched TLS fingerprint, never one alone.


Pace and behave like a human

Behavioral scoring is the layer people forget. Even with a flawless fingerprint and a residential IP, ten requests per second from one session reads as a machine. DataDome watches cadence and navigation shape, so a scraper that acts like a browsing human survives far longer.

  • Warm up. Load the homepage first and let the DataDome tag set a cookie before you request deep pages. Jumping straight to a product URL with no cookie is a classic bot pattern.
  • Add jittered delays. Random pauses between requests beat a fixed interval, because a perfectly regular heartbeat is itself robotic.
  • Cap concurrency per IP. A handful of parallel requests per exit IP, not hundreds. Spread load across the pool.
  • Follow a plausible path. Category, then listing, then detail. Real users don't teleport across a hundred unrelated URLs in a minute.
import random, time

def polite_delay(lo=1.5, hi=5.0):
    """Human-like jittered pause between requests."""
    time.sleep(random.uniform(lo, hi))

If the site publishes a Crawl-delay in robots.txt, treat it as a hard floor, not a suggestion. Slower crawling costs you throughput today and saves you from a blanket ban tomorrow. It's the cheapest anti-detection technique there is, and the most ethical.


Bypass DataDome with the SparkProxy Scraping API

Maintaining a real browser farm, a residential pool, current TLS fingerprints, and behavioral pacing is a standing engineering job, because DataDome keeps moving. A scraping API collapses those layers into request parameters and keeps them current on the provider's side. The scraping API vs self-managed proxies comparison covers when that trade is worth it; for a hard target like DataDome, it usually is.

The SparkProxy Scraping API takes a target URL and handles the proxy, the real browser render, and the anti-bot layers for you. Each parameter maps to one of the detection layers above:

curl -G "https://scrape.sparkproxy.io/api/v1" \
  -H "X-API-Key: YOUR_API_KEY" \
  --data-urlencode "url=https://example.com/listings" \
  --data-urlencode "render_js=true" \
  --data-urlencode "premium_proxy=true" \
  --data-urlencode "country_code=US" \
  --data-urlencode "stealth=true" \
  --data-urlencode "wait_for=.product-grid" \
  --data-urlencode "json_response=true"

The same request in Python, with each flag annotated by the layer it satisfies:

import requests

def fetch(url):
    r = requests.get(
        "https://scrape.sparkproxy.io/api/v1",
        headers={"X-API-Key": "YOUR_API_KEY"},
        params={
            "url": url,
            "render_js": "true",        # real Chromium runs DataDome's JS check
            "premium_proxy": "true",    # residential exit, not a flagged datacenter ASN
            "country_code": "US",        # geo-match the site's expected audience
            "stealth": "true",           # homepage pre-warm, forced referrer, idle delays
            "wait_for": ".product-grid", # wait past any interstitial until content renders
            "json_response": "true",     # envelope with status_code + metadata
        },
        timeout=120,
    )
    return r.json()

render_js=true runs a genuine browser so the JavaScript fingerprint and any rt:"c" challenge resolve. premium_proxy=true routes through a residential IP, clearing the reputation layer. stealth=true adds a homepage pre-warm, a forced Google referrer, and extended idle delays, which is the behavioral layer handled for you. wait_for holds until your real content appears, so you don't capture a DataDome interstitial by mistake. With json_response=true, the reply is an envelope carrying status_code, credits_used, and a result_url you fetch for the rendered body. On the SparkProxy price sheet, a residential request with JS rendering costs 25 credits, and stealth plus country_code add 5 credits each, so turn them on for hard targets and leave them off for easy ones.


A resilient retry loop that reads DataDome signals

Tie it together with a loop that branches on the decoded status instead of blindly retrying. With json_response=true, the envelope's status_code mirrors what the target returned, so you can act on a 403, a 429, or a 530 differently.

import time
import requests

API = "https://scrape.sparkproxy.io/api/v1"
KEY = "YOUR_API_KEY"

def scrape(url, max_tries=4):
    params = {
        "url": url,
        "render_js": "true",
        "premium_proxy": "true",
        "country_code": "US",
        "stealth": "true",
        "json_response": "true",
    }
    for attempt in range(1, max_tries + 1):
        env = requests.get(API, headers={"X-API-Key": KEY},
                           params=params, timeout=120).json()
        code = env.get("status_code")

        if code == 200:
            body = requests.get(env["result_url"], headers={"X-API-Key": KEY})
            return body.text                 # rendered HTML, past DataDome

        if code == 429:                      # rate limited: exponential backoff
            time.sleep(2 ** attempt)
            continue

        if code in (403, 530):               # blocked or scrape failed: retry, already max-stealth
            time.sleep(1.5 * attempt)
            continue

        raise RuntimeError(f"Unexpected DataDome status {code} for {url}")

    raise RuntimeError(f"Gave up on {url} after {max_tries} tries")

The loop already runs with premium proxy and stealth on, so a 403 retry gets a fresh residential IP and a new browser session rather than the same doomed request. A 429 backs off exponentially instead of hammering the origin. A 530 (the API's "all upstream attempts failed" code) gets a short, growing pause. When nothing clears after a few tries, the honest move is to stop, respect the site's signal, and revisit whether the data is worth pursuing at all.


Frequently asked questions

FAQ

Scraping public data is broadly permitted in many jurisdictions, but "bypassing DataDome" does not grant a legal exemption. Legality depends on what you collect and how: stay on public pages, honor robots.txt and the site's Terms of Service, avoid personal data, and never touch content behind a login. When in doubt, get written permission or use the site's official API.

Because two layers give it away at once. Python's requests sends a TLS fingerprint (JA3/JA4) that matches no real browser, and scripts usually run from a datacenter IP that DataDome already distrusts. The mismatch between a browser-like User-Agent and a Python TLS handshake, combined with a flagged ASN, is enough for an immediate 403.

Sometimes, for endpoints that only score the network and header layers. Pairing curl_cffi (with impersonate="chrome124") and a residential IP can pass those. But when DataDome serves a JavaScript challenge (rt:"c"), there is no runtime to answer it without a real browser or a rendering API, so a bare HTTP client will stall on the challenge.

The datadome cookie is a first-party cookie that stores your client identifier and device validation state; a valid one is set after the JavaScript tag reports a clean fingerprint or you pass a CAPTCHA. Don't replay one cookie across many exit IPs. The same identifier arriving from dozens of addresses is a pattern DataDome flags, so bind each cookie to the session and IP that earned it.

No. A residential IP clears the reputation layer, which is often the fastest block, but DataDome still scores your TLS fingerprint, JavaScript environment, and behavior. A residential IP behind a Python client with a Python TLS handshake still gets blocked. Combine a clean IP with a real browser or a matched fingerprint, plus human-like pacing.

A challenge (rt:"c") means DataDome wants proof of a real device. Solve it inside a genuine browser session so the JavaScript runs and refreshes your datadome cookie, or route the request through a rendering API that runs a real browser for you (for the SparkProxy Scraping API, render_js=true with stealth=true). Do not brute-force the CAPTCHA endpoint; that behavior gets the whole session banned.


Limited-time · 50% off

Get 50% off your first purchase

Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.

Offer ends soon — claim it before it's gone

Claim Discount

About the Author

This guide was written by the SparkProxy Technical Team. SparkProxy builds and operates global datacenter and residential proxy networks plus a managed Scraping API, so we see how anti-bot systems like DataDome score traffic from both sides of the request every day. The patterns here reflect current behavior validated against Playwright, playwright-stealth, curl_cffi 0.7+, and the SparkProxy Scraping API as of July 2026. Our aim is a straight, ethical explanation of how DataDome works and how to collect public data without abusing anyone's infrastructure, not a promise of a permanent bypass, because no such thing exists.

Citations: curl_cffi, browser TLS impersonation for Python · SparkProxy Scraping API documentation

Keep reading

Related articles