parsa
Get started

How DataDome works: the interstitial, the slider and the datadome cookie

How DataDome blocks bots: the 403 page and its dd object, the interstitial device check, the slider captcha, tags.js beacons and how to debug a failed solve.

Updated 12 min read

In short

  • DataDome scores every request; a trusted visitor carries a datadome cookie, anyone else gets a 403 page holding var dd = {…}.
  • rt:'i' in that object is the interstitial device check, rt:'c' the slider captcha; both run from geo.captcha-delivery.com.
  • The interstitial runs a bytecode VM and a WebAssembly attestation that probe the browser; a POST of the result answers view: redirect with a new cookie.
  • On pages it doesn't challenge, tags.js sends ch and le beacons that keep the cookie fresh; the IP, time zone, language and client hints must agree throughout.

DataDome is a bot protection service that sits in front of a site and scores every request. A visitor it trusts carries a datadome cookie. A visitor it doesn’t gets a 403 instead of the page, and that 403 holds one of two challenges: an interstitial device check that runs without asking anything, or a slider captcha. On pages it doesn’t challenge, a script called tags.js reports on the visit in the background and keeps the cookie fresh.

This guide follows all three on the wire: how to recognise DataDome, how to turn the block page into a challenge link, what the device check probes and how its answer is sealed, how the slider is checked, what the tag’s beacons carry, and what DataDome looks at besides the payload. It ends with a debugging checklist.

The short version: the payload is necessary, not sufficient. DataDome scores the whole session.

How to tell a site uses DataDome

  • The block page. A 403 whose body is a tiny HTML page with an inline var dd={…} object and a script from ct.captcha-delivery.com. The 403 also sets a first datadome cookie.
  • The challenge host. The challenge itself runs in a frame from geo.captcha-delivery.com, under /interstitial/ or /captcha/.
  • The tag. Ordinary pages set window.ddjskey (a 30-character hex string) and load tags.js, from js.datadome.co or from the site’s own domain.
  • The beacons. Form posts to https://api-js.datadome.co/js/, or to the site’s own /js/ path when it serves the tag first-party.

A blocked request looks like this:

GET /products/42 HTTP/2
Host: shop.example.com
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) …
accept-language: en-US,en;q=0.9

HTTP/2 403
content-type: text/html
set-cookie: datadome=<initial value>; …

<html><head><title>shop.example.com</title></head>
<body>
<script>var dd={'rt':'i','cid':'<cid>','hsh':'<hash>','b':<b>,'s':<s>,'e':'<e>','host':'geo.captcha-delivery.com'}</script>
<script src="https://ct.captcha-delivery.com/i.js"></script>
</body></html>

Reading the dd object

Everything the next step needs is in dd. Parse it out of the body.

Key What it is Where it goes
rt The challenge type: 'i' is the interstitial, 'c' the slider captcha Picks the path, /interstitial/ or /captcha/
cid The challenge’s initial id initialCid
hsh A hash the page carries hash
cookie The datadome cookie the 403 set (the same value is in your jar) cid
s, b Numbers, passed through as they are s, b
e, t Present on some pages only, passed through e, t
host The challenge host, geo.captcha-delivery.com The link’s host

Note the naming trap: dd.cid becomes initialCid, and the link’s cid is the cookie. They are easy to swap.

DataDome uses one cookie, datadome, and rewrites it at every stage.

Cookie Set by What it means Lifetime
datadome The 403 block response, as set-cookie An id for the blocked visit; it goes into the device link —
datadome The JSON answer to POST /interstitial/, in its cookie field The device check was accepted; retry the page with it Max-Age=31536000 (a year) in the answers we have seen
datadome The JSON answer to GET /captcha/check, in its cookie field The slide was accepted for scoring, which is not the same as cleared —
datadome The answer to each tags beacon The visit’s score refreshed; after ch, also part of the key for the le payload —

The challenge answers hand you the cookie inside JSON, not as a header your client stores for you. Copy the value into your jar yourself, with the domain and path it names. A long Max-Age is not a year of free passage: DataDome re-scores every request, and a session that changes IP or fingerprint is challenged again.

The interstitial, step by step

From dd, build the URL of the challenge frame, URL-encoding every value. referer is the URL you were blocked on. Leave out t and e when the page has none.

https://geo.captcha-delivery.com/interstitial/?initialCid=<dd.cid>&hash=<dd.hsh>&cid=<datadome cookie>&t=<dd.t>&referer=<blocked URL>&s=<dd.s>&b=<dd.b>&e=<dd.e>&dm=cd

2. Fetch the challenge page

GET /interstitial/?initialCid=…&hash=…&cid=…&referer=…&s=…&b=…&e=…&dm=cd HTTP/2
Host: geo.captcha-delivery.com
user-agent: <the same agent as before>
accept-language: <the same as before>

Keep the body. Most builds inline their challenge bundle in it. Some link it instead, with a hashed file name that changes when DataDome rotates the build:

GET /interstitial.<build hash>.js HTTP/2
Host: ct.captcha-delivery.com

Fetch it with the same client. In a browser, the bundle now runs the device check described below and builds a form body. That is the step a plain HTTP client can’t do.

3. Post the answer

POST /interstitial/ HTTP/2
Host: geo.captcha-delivery.com
content-type: application/x-www-form-urlencoded
sec-ch-ua-platform: "macOS"
sec-ch-ua-full-version-list: …
…

<form body built by the device check>

DataDome answers with JSON:

{
  "cookie": "datadome=<value>; Max-Age=31536000; Domain=.example.com; Path=/; Secure; SameSite=Lax",
  "view": "redirect",
  "url": "https://shop.example.com/products/42"
}

view is the verdict. redirect means the check passed: store the cookie and retry the page at url. captcha means DataDome accepted the post, scored it, and wants the slider. The status is 200 either way, so branch on view, not on the status.

What the device check collects

A bytecode VM

The interstitial bundle is about half a megabyte of obfuscated JavaScript (530 KB in one build we measured). Most of it is a small interpreter and the program it runs: tens of thousands of instructions of custom bytecode, which executes a few hundred thousand steps per check. Each opcode is ordinary stack-machine work: push a value, read a property, add, xor, jump. The program changes with each build, so anything that hard-codes one build breaks on the next rotation.

Almost all of that work is probing the browser and hashing the results. What changes per request is the clock, the run’s own randomness, and the challenge’s ids and seed.

A WebAssembly attestation

Alongside the VM, the bundle carries a WebAssembly module whose layout is specific to the build. It ties what was measured to this challenge and this run, so a result can’t be lifted from one check and reused on another.

The signals

Concretely, the check reads:

  • Rendering: WebGL and GPU values, and a canvas digest.
  • Fonts: probing of installed font families, plus the @font-face families the page’s stylesheets declare.
  • DOM and CSS: how the document and computed styles behave.
  • The window object: a hash over the window’s properties, which differs between browsers and builds.
  • The engine: Error().stack strings, which must be in the exact format of the Chrome build you claim.
  • Speech voices and the keyboard map.
  • Hardware and platform: device memory, CPU architecture and platform, the same values the client hints state.
  • Locale: the time zone name and its current offset, navigator.language and the language list.
  • Integrity: a value derived from the bundle itself, so a modified bundle gives itself away.

How it is encoded

The form body carries two views of the same device. One is a long list of named fields, about 240 in the builds we read. The other is an opaque blob the VM produces, hashed and sealed at the end of the run; the form calls it plv3, and the field list beside it plv2. DataDome cross-checks the two, so a field list that disagrees with the blob fails even when each looks plausible on its own.

The whole thing is encoded, signed and encrypted, keyed from the challenge (its seed and ids), the clock and per-run randomness. A captured or cached answer is a replay and dies; each challenge needs its own run.

Take care with time zone and languages. The check reports the browser’s zone and offset, and DataDome sees the IP the post came from: a Tokyo exit reporting Europe/Berlin is a contradiction it reads without any fingerprinting. The same goes for navigator.language against your accept-language header.

The slider captcha

When DataDome trusts a visit less, the 403’s dd has rt:'c'. The frame shows a background image with a gap and a piece to drag into it. DataDome changes its captcha now and then, and reports differ on how widely the slider is still served; in our own runs as late as October 2026, rt:'c' served this slider.

1. Fetch the captcha page and the puzzle

The device link is built as for the interstitial, with /captcha/ in the path and without b:

https://geo.captcha-delivery.com/captcha/?initialCid=<dd.cid>&hash=<dd.hsh>&cid=<datadome cookie>&t=<dd.t>&referer=<blocked URL>&s=<dd.s>&e=<dd.e>&dm=cd
GET /captcha/?initialCid=…&hash=…&cid=…&t=…&referer=…&s=…&e=…&dm=cd HTTP/2
Host: geo.captcha-delivery.com

The page names a captchaChallengePath: the puzzle .jpg, and next to it the piece as .frag.png. Fetch the .jpg with the same client. Like the interstitial, the page may link its bundle (ct.captcha-delivery.com/captcha.<build hash>.js) instead of inlining it.

2. Find the gap and move like a person

The gap can be found from the background image alone, without the piece: the cut-out leaves an edge and a shading change that image analysis can locate.

Knowing the offset is the easy half. The check also describes the slide: a pointer path from the handle to the gap, with the uneven speed and timing a hand produces. A straight line at constant speed is not a person. The same device check as the interstitial runs alongside, so everything in the previous section applies here too.

3. Wait, then check

The slide takes a few seconds. Its answer is a GET whose query carries the encoded slide and device data:

GET /captcha/check?<encoded slide and device data> HTTP/2
Host: geo.captcha-delivery.com
sec-ch-ua-platform: "macOS"
…

Send it no sooner than the slide described would have finished. A check that describes a five-second slide but arrives half a second after the captcha page loaded gives itself away.

The check can answer 200 with a cookie in its JSON even when the slide failed. DataDome accepts the request and then scores it, and the verdict shows up on your next request to the site. We have seen checks answer 200 with a fresh cookie and the page still answer 403 with it. The only proof is the retry: fetch the blocked page with the new cookie and look for real content, not another dd page.

tags.js and its beacons

tags.js blocks nothing. It watches the page and posts two beacons, which DataDome scores and answers with a refreshed datadome cookie. A session that never sends them is more likely to meet the interstitial or the slider later.

The page names the site’s key and, optionally, the endpoint:

window.ddjskey = '<30-character hex key>';
window.ddoptions = { endpoint: "https://api-js.datadome.co/js/" };
Beacon Sent Carries
ch As the page loads Device and page signals; the event counters are an empty list
le About ten seconds after the visitor’s first mouse move The same, plus statistics over mouse, pointer, key, scroll and touch events, and the event counts

Both are form posts to the endpoint the page names:

POST /js/ HTTP/2
Host: api-js.datadome.co
content-type: application/x-www-form-urlencoded

jspl=<encrypted signals>&eventCounters=%5B%5D&jsType=ch&ddk=<key>&Referer=<page URL>&request=<…>&responsePage=origin&ddv=<tag version>

jspl is the encrypted collector output, and part of its key comes from the current datadome cookie. That is why order matters: send ch, store the cookie its answer sets, and only then build le with that cookie. An le keyed from the old cookie is unreadable to DataDome. Sites that serve the tag first-party also take the beacons on their own /js/ path. Fetch the tag the page actually loads, from wherever it loads it.

What DataDome checks besides the payload

The six client hints

Chrome sends User-Agent Client Hints, and the high-entropy ones once a site asks for them. Your requests have to carry hints that describe the same device as the payload:

{
  "sec-ch-device-memory": "16",
  "sec-ch-ua-mobile": "?0",
  "sec-ch-ua-arch": "\"arm\"",
  "sec-ch-ua-platform": "\"macOS\"",
  "sec-ch-ua-model": "\"\"",
  "sec-ch-ua-full-version-list": "\"Chromium\";v=\"152.0.7977.76\", \"Not?A_Brand\";v=\"24.0.0.0\", \"Google Chrome\";v=\"152.0.7977.76\""
}

Send them on the post to DataDome and on every request after it. A payload describing one device, from requests whose hints describe another or none, is a contradiction.

TLS and HTTP/2

The edge fingerprints your TLS handshake (JA3, JA4) and your HTTP/2 settings, window update and pseudo-header order. Go’s net/http, Python’s requests and plain curl are each recognisably not Chrome. You need a client that reproduces the Chrome build in your user agent.

Header order

Browsers send headers in a stable order; many HTTP libraries sort them. Match the browser you claim on every request, including the posts to geo.captcha-delivery.com and the beacon endpoint.

IP, time zone and language

One session is one exit IP. The payload’s time zone must be that IP’s, and its languages your accept-language header’s, in order. A proxy that rotates between the 403 and the post is a session visibly changing.

The client that fetched the 403 must fetch the challenge, post the answer and retry the page, with one jar. Read the datadome value fresh before every step. The Akamai guide covers the same session rules from another vendor’s side.

Common failures and how to debug them

  • The interstitial answers view: "captcha". The post was read and scored low. Check TLS fingerprint, header order, client hints and whether your IP is a datacentre range before you suspect the payload.
  • The slider check gives a cookie and the page is still 403. Scored after the fact. Check that you waited for the slide’s duration, kept the same exit IP and sent the hints. Then check the same things as above.
  • The cookie seems ignored on retry. Wrong domain in the jar, the old value kept alongside it, or a different exit.
  • The challenge fails on input that looked right. Usually the html is not the page the device link names: a stale fetch, or a link rebuilt from a different 403. Fetch the challenge again rather than retrying the same bytes.
  • It worked yesterday and fails today on every request. The build rotated. Look at the bundle’s file name; anything tied to the old build needs the new one.
  • Sessions pass the first check and get challenged a few pages later. You aren’t sending the tag’s beacons, or you sent le straight after ch instead of after a plausible delay.
  • le has no effect. It was built with the cookie from before ch.
  • A first-party site ignores your beacons. You posted to api-js.datadome.co while the page names its own /js/ path, or built the beacon from DataDome’s tag rather than the site’s.
  • The 403 page has no challenge to solve, only a notice that you have been blocked. The visit was refused outright, usually on the IP. No payload helps: change the exit, then start a fresh session.
  • Everything looks right and it still fails. Compare time zone, languages and client hints with your exit IP and headers, then your user agent’s Chrome version with the TLS fingerprint your client produces.

Solving DataDome with Parsa

Parsa computes the payloads; your client makes every request to the site and to DataDome. For the interstitial, send deviceLink and the challenge page as html. For the slider, add the puzzle as base64 puzzle; the reply’s payload is the /captcha/check URL, and waitMs says how long to wait before you GET it. For the tags, send ddk, referer, type and cid. Every request carries userAgent, ip and acceptLanguage, and every reply returns the six client hints as headers. A 500 means the solve failed: fetch the challenge again and retry once.

One limit, stated plainly: Parsa accepts Chrome on macOS user agents only for now. Windows agents are refused with a 400. In our live runs against a forced captcha, macOS devices cleared the page 4 of 4 and Windows devices 0 of 2. We haven’t found the cause, so we refuse rather than bill you for a cookie that won’t work. Timings and live results are on the benchmarks, and only 200s are billed: see pricing.

DataDome solver APISkip the browser. Send the challenge, post the answer.Interstitial, Slider, Tags. Pay per successful solve, never for a failure.

More guides