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 fromct.captcha-delivery.com. The 403 also sets a firstdatadomecookie. - 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 loadtags.js, fromjs.datadome.coor 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.
The datadome cookie
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
1. Build the device link
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-facefamilies 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().stackstrings, 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.languageand 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.
A 200 with a cookie is not proof
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.
Cookie jar continuity
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
htmlis 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
lestraight afterchinstead of after a plausible delay. lehas no effect. It was built with the cookie from beforech.- A first-party site ignores your beacons. You posted to
api-js.datadome.cowhile 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.