Incapsula is the name most people still use for Imperva’s edge protection. Imperva is now part of Thales, and the product that deals with bots is Imperva Advanced Bot Protection. On a site that runs it, a JavaScript file collects details about your browser, posts them back to the site, and gets a signed token in return. That token is stored as a cookie called reese84. Requests without a valid, recent one get challenged or blocked.
This guide explains how to recognise it, what the flow looks like on the wire, what the script actually is (a bytecode virtual machine, not ordinary obfuscated JavaScript), what the sensor describes, and what the server checks besides the sensor. It ends with a debugging checklist.
It covers the static variant, the one that runs silently in the background. The dynamic variant, which shows a “Pardon Our Interruption” page and asks for a proof of work, is mentioned where it matters but not taken apart.
Imperva, Incapsula and reese84
Incapsula was a CDN and web application firewall that Imperva bought, and the name stuck long after the branding moved on. You will see “Imperva”, “Incapsula” and “Advanced Bot Protection” used for overlapping things. reese84 is the name of the cookie, and by extension of the script and sensor that produce it.
Imperva’s edge also sets its own session cookies on protected sites, visid_incap_<id> and incap_ses_<id> (and on some sites nlbi_<id>). They travel with reese84 and belong in the same jar. ___utmvc is something else: the result of Imperva’s older JavaScript challenge, served from /_Incapsula_Resource, which this guide does not cover.
How to tell a site uses reese84
Open the browser’s developer tools on the protected page and look for any of these:
- A cookie named
reese84. Its value is a long opaque string; the ones we have seen start with3:. - An
x-d-tokenrequest header. Some sites send it on their own requests. - A script served from the site’s own domain at an odd path. The path is made of a few random words, for example
/maple-orbit-x, not a vendor CDN host. Some sites add a query string such as?s=…. - A POST to that same path with
?d=<host>. This is the sensor. It is the most reliable sign: same path as the script, adparameter naming the page’s host, and atext/plainbody. - A “Pardon Our Interruption” page. This means the site uses the dynamic variant, which adds a proof of work.
GET /maple-orbit-x?s=AbCdEf HTTP/2
Host: shop.example.com
Accept: */*
Referer: https://shop.example.com/
POST /maple-orbit-x?d=shop.example.com HTTP/2
Host: shop.example.com
Content-Type: text/plain; charset=utf-8
In our captures the path stayed the same for a site, but others report it changing between deliveries, so read it from the page in each session rather than hard-coding it.
The cookies
| Cookie | Set by | What it means | Lifetime |
|---|---|---|---|
reese84 |
The script, from the token in the answer to the sensor POST |
A server-signed, opaque token saying this client passed a recent interrogation. It cannot be forged. | Renew before renewInSec seconds; we have seen 695 and 896 |
Imperva’s other cookies, for reference:
| Cookie | Set by | What it means | Lifetime |
|---|---|---|---|
visid_incap_<id> |
Imperva’s edge, on protected sites | Commonly described as a visitor identifier; keep it with reese84 |
— |
incap_ses_<id> |
Imperva’s edge, on protected sites | Commonly described as a session cookie; keep it with reese84 |
— |
nlbi_<id> |
Imperva’s edge, on some sites | Commonly described as load-balancer state | — |
___utmvc |
A classic Incapsula JavaScript challenge | The classic challenge’s result | — |
The flow, step by step
1. Load the page
Your client requests the protected page as a browser would. The HTML references the reese84 script. Keep the cookie jar from this response: everything that follows belongs to the same session.
2. Fetch the script
GET the script URL exactly as the page references it, query string included, with the same headers your client sends on every other request.
GET /maple-orbit-x?s=AbCdEf HTTP/2
Host: shop.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/<version> Safari/537.36
Accept: */*
Accept-Language: en-US,en;q=0.9
Referer: https://shop.example.com/
Cookie: <whatever the page response set>
The response body is the whole program. In a build we measured, it was about 800 KB.
3. The script builds a sensor
In a browser, the script now runs, collects its signals and serialises them into a sensor. The details are in the next sections.
4. Post the sensor
The sensor goes back to the script’s path, with d set to the page’s host. The body is a JSON document, but the content type is text/plain, and in a browser the request goes over HTTP/2.
POST /maple-orbit-x?d=shop.example.com HTTP/2
Host: shop.example.com
Content-Type: text/plain; charset=utf-8
Origin: https://shop.example.com
Referer: https://shop.example.com/
{"solution":{"interrogation":{"p":"<base64>","st":<unix seconds>,"sr":<build constant>,"cr":<nonce>,"og":2},"version":"stable"},"old_token":null,"error":null,"errors":[],"performance":{"<label>":<ms>,"<label>":<ms>,"total":<ms>}}
5. Read the answer and set the cookie
A successful answer is a small JSON object:
{ "token": "3:<opaque>", "renewInSec": 896, "cookieDomain": ".example.com" }
Store token as the reese84 cookie on cookieDomain, then retry the request that needed it. cookieDomain can also be null; in that case scope the cookie to the host you posted to.
6. Renew before it runs out
The token expires. renewInSec says when: the values we have seen were 695 and 896 seconds, so roughly 12 to 15 minutes. A renewal does not repeat the whole interrogation. The script posts only the previous token, as a bare JSON string, to the same URL:
POST /maple-orbit-x?d=shop.example.com HTTP/2
Host: shop.example.com
Content-Type: text/plain; charset=utf-8
"3:<opaque>"
The answer has the same shape as in step 5. So the full sensor is needed once per session; after that, renewal keeps it alive. If renewal fails, run steps 2 to 5 again.
Inside the script: a bytecode virtual machine
If you open the script expecting to read the collection logic, you will be disappointed. The file is an interpreter, and the actual program is embedded in it as encoded bytecode. The JavaScript you can read describes how to execute instructions; what those instructions do lives in the data. Reading the interpreter tells you almost nothing about which signals are collected or how they are encoded.
Three properties of the script matter in practice:
- Each build is different. The embedded program, its constants and its per-field ciphers change between builds. A tool that works on one build has to be able to read the next one, not carry a fixed copy.
- There is a per-build constant in the envelope. The
srvalue in the sensor comes from the script itself. A sensor with ansrfrom another build does not belong to the script you were served. - The program can be read without being run. Because the program is data, it can be lifted statically: decoded and walked without executing it in a browser or a JavaScript engine. In the build we studied, close to 300 distinct field transforms came out of that walk.
What the sensor contains
The envelope
The POST body is a JSON object, and most of it is plain:
| Field | What it is |
|---|---|
solution.interrogation.p |
The collected data: a binary record stream, base64-encoded |
solution.interrogation.st |
A unix timestamp in seconds |
solution.interrogation.sr |
The per-build constant, read from the script |
solution.interrogation.cr |
A random nonce generated for this request |
solution.interrogation.og |
A small integer; 2 in every sensor we have seen |
solution.version |
stable |
old_token |
null on a first interrogation |
error, errors |
null and an empty list when collection went well |
performance |
How long collection took, keyed by the script’s own timing labels, plus a total |
The important relationship is between cr and p. Every field key inside p is derived from cr. Change the nonce, or post a p with a different envelope, and nothing in it decrypts to what the server expects.
The record stream
p is a binary buffer. It starts with a short header and then holds a sequence of typed records, one per collected value. The types cover what you would expect from serialising browser data: null and undefined, booleans, numbers, strings, the start and end of arrays and objects, and nested sub-payloads, which are whole record streams embedded inside a record. Each record carries a small type tag and an identifier for the field it belongs to.
Every field is enciphered on its own, with a chain of small reversible byte transformations that differs from field to field. The key material for each chain is derived from the nonce cr, and each field’s ciphertext also depends on the record before it, so no plaintext tag, identifier or string survives anywhere in p.
Two consequences follow. You cannot patch a captured sensor, because every field depends on the nonce and on its neighbours. And a nested sub-payload has to be rebuilt under the new nonce, not copied.
What the records describe
A decoded session we worked from held 273 records. They fall into a handful of categories:
| Category | Examples |
|---|---|
| Browser identity | User agent, navigator.appVersion, vendor, platform, language |
| Client hints | Brand list with Chrome’s greased brand token, full version, platform version, architecture, bitness |
| Screen and window | Screen size, available size, inner and outer window size, orientation |
| Hardware | CPU core count |
| Graphics | WebGL vendor and renderer strings, GL parameters, extensions and shader precision formats |
| Rendering | Canvas output as base64, and digests of collected values |
| Fonts and permissions | Installed fonts, permission states |
| Environment | The page’s host, and the presence or absence of browser features such as ActiveXObject |
| Time | Several wall-clock timestamps, plus the collection timings in performance |
Many of these must agree with each other, and the server can check that. The user agent must match appVersion and the major versions in the brand list. The client-hint platform must match the platform in the user agent. The available screen size cannot exceed the screen size, and the inner window cannot exceed the outer one. The graphics strings must fit the operating system: Chrome on Windows reports an ANGLE renderer backed by Direct3D 11, while Chrome on a Mac reports Apple’s GPU through Metal. A Windows user agent with a Mac GPU is a contradiction, and so is a canvas digest rendered on one GPU sent next to the renderer string of another.
How the server checks beyond the payload
A correct sensor is necessary and not sufficient. Imperva sees the whole request, not just the body, and most failures that look like a rejected sensor come from somewhere else.
TLS fingerprint. The edge fingerprints your TLS handshake, the kind of fingerprint JA3 and JA4 summarise: cipher suites, extensions and their order, supported groups. Python’s requests, Go’s net/http and plain curl are each recognisably not Chrome. Your client must produce a Chrome handshake, and keep producing the same one for the whole session.
HTTP/2 fingerprint. The sensor POST goes over HTTP/2 in a real browser, so send it over HTTP/2. HTTP/2 has its own fingerprint: the SETTINGS frame values, the initial window update, and the order of the pseudo-headers (:method, :authority, :scheme, :path). These must match the browser your user agent claims.
Header order and values. Browsers send headers in a stable order. Many HTTP libraries sort them or emit them in map order. Beyond order, the values you send must match the values inside the sensor: the user-agent header must be the user agent the sensor describes, and accept-language must match the sensor’s language.
IP consistency. Use one exit IP for the whole session. A session that fetches the script from one address and posts the sensor from another is a session the edge can see changing. An exit whose country fits the language you claim is also the safer choice.
Cookie jar continuity. One session means one cookie jar. The client that loaded the page must be the one that fetches the script, posts the sensor and makes the protected requests.
Time. The envelope’s st and the timestamps inside p say when the session started and when it was interrogated. They must be plausible for when the sensor arrives, so a captured sensor goes stale.
The same rules apply to Akamai Bot Manager and DataDome.
Common failure modes and how to debug them
- If the sensor POST returns something other than a token, check the request shape first: path identical to the script’s,
?d=set to the page’s host (not the script’s),Content-Type: text/plain; charset=utf-8, HTTP/2, and the body posted byte for byte. Parsing the JSON and dumping it again can reorder keys or change escaping. - If you get a token but the protected request is still blocked, check where the cookie went. It must be named
reese84and scoped tocookieDomain. Then check that the protected request goes out with the same TLS fingerprint, header order, user agent and IP as the sensor POST. - If it works for a while and then stops, you are probably not renewing. Track
renewInSecand renew before it, or rerun the full flow. - If it fails intermittently, look for something that changes mid-session: a rotating proxy, a second HTTP client used for one of the steps, or a cookie jar that is not shared between them.
- If the site shows “Pardon Our Interruption”, it is serving the dynamic variant. That needs a proof of work that the static flow does not compute.
- If the “script” you fetched is HTML, you were blocked before you got to it, or you dropped the query string. Fetch it with the full URL,
?s=included, from the session that loaded the page. - If everything matches and the sensor is still rejected, look at the device you claim. A Windows user agent with a Mac GPU string, client hints that disagree with the user agent, or an
accept-languageheader that differs from the sensor’s language are all contradictions the server can read. - If the site serves
___utmvcfrom/_Incapsula_Resource, it runs Imperva’s older challenge, not reese84, and solving reese84 will not change it. - If you dropped
incap_ses_*orvisid_incap_*, put them back: they are the edge’s own session, and areese84token without them describes half a session.
Solving Incapsula with Parsa
Parsa builds the sensor; your client does everything else. Send POST https://api.parsalabs.io/incapsula/reese84 with your API key (Authorization: Bearer parsa_…) and a JSON body of userAgent, pageUrl, script (the body of your GET to the script URL), scriptUrl, ip and acceptLanguage. The answer is {"payload": "<sensor JSON>"}: post it unchanged to scriptUrl?d=<page host> as text/plain; charset=utf-8 and store the returned token as reese84. Parsa never contacts the protected site, so TLS, HTTP/2, headers and IP stay on your side. The script is lifted statically, without a browser: on an Apple M5 Pro, the first sensor for a new 800 KB script took about 67 ms including the lift, and later ones about 0.5 ms (mean of 50). The device described is one of 53 real machines (27 macOS, 26 Windows), with its language taken from acceptLanguage.
What it does not do: the dynamic “Pardon Our Interruption” variant and its proof of work (pow is accepted and ignored), or any browser other than Chrome on Windows or macOS (a 400). Details are on the Incapsula solver page and its benchmarks; you pay only for successful solves, see pricing.