HUMAN's _px3 cookie and the PXHD flow explained
Load a page behind HUMAN’s Bot Defender, formerly PerimeterX, and somewhere in the response you will find a Set-Cookie: _px3=... header carrying a long opaque blob split by colons. It looks like session state. It is not. That value is the wire form of a decision the protection layer already reached about your browser, signed so that the customer’s own servers can re-check it without phoning home on every request. Next to it sit two quieter companions, _pxvid and _pxhd, and the three are read together.
So what is actually inside _px3, what does the server do with it, and how do _pxhd and _pxvid change the picture? This post stays in the cookie lane. We will trace where the risk cookie comes from, the exact byte layout that the open-source enforcers parse, the encryption and the HMAC that bind it, the score and action fields it carries, why the signature is tied to your user agent, and where _pxhd and _pxvid fit. The full sensor payload, the press-and-hold challenge, and the cross-customer detection network each get their own treatment elsewhere. Here the subject is the cookie itself.
The names, and why there are two of them
Start with the branding, because it trips people up. The product you see in marketing is HUMAN. The strings you see in cookies and JavaScript paths still say _px. PerimeterX was founded in 2014 in San Mateo, and on a merger announced in 2022 it combined with HUMAN Security (itself founded in 2012 as White Ops). The PerimeterX cofounders joined HUMAN’s board, with one of them taking the CTO seat. The plumbing kept its old prefix. If you want the full account of what the rebrand did and did not change under the hood, that is its own post: PerimeterX to HUMAN: the rebrand and what changed.
HUMAN’s own cookie documentation lists the set you will see in a browser’s storage inspector: _px, _px2, _px3, _pxhd, _pxvid, plus a handful of operational ones like pxcts and _pxde. The three that matter for the risk decision are _px3, _pxvid, and _pxhd. The vendor describes two versions of what it calls the Risk Cookie, v2 (_px2) and v3 (_px3), and says each enforcer supports one or both, with v3 the current recommendation. v1 (_px) is the oldest format and is effectively legacy. Most live sites today set _px3.
The reason the version number is visible in the cookie name at all is that the format changed in a way the server has to know about before it can parse the value. A v2 cookie and a v3 cookie are decrypted and validated differently, and the most consequential difference is in how the signature is computed. We will get to that.
What the risk cookie is for
The risk cookie is a signed verdict. When a browser passes HUMAN’s client-side checks, the backend issues a token that encodes a risk score and an instruction, and it hands that token to the browser as _px3. On the next request the browser sends the cookie back. The customer’s enforcer, an Nginx module, a server-side SDK, a CDN worker, decrypts it, checks the signature, reads the score, and decides whether to serve the page or to challenge or block.
The point of signing the verdict and pushing it to the client is latency. Without a cookie, every request would need a server-to-server call to HUMAN’s risk API before the origin could respond. With a valid _px3 in hand, the enforcer can make the pass-or-block call locally, in-process, without that round trip. The cookie is a cache of a decision, cryptographically sealed so a client cannot forge or edit it. That design is spelled out in PerimeterX’s own patents. US 10,708,287 B2, Analyzing client application behavior to detect anomalies and prevent access (issued July 2020, assignee PerimeterX, Inc.), describes a security verification system that runs tests inside the user-agent, compares the results against other user-agents, and then “provides a token to the user-agent application to access the application server.” A companion patent, US 10,951,627 B2, Securing ordered resource access, frames the same object as a risk token that secondary servers consult to authorize access, carrying a risk score and an expiration time.
The cleanest place to read the mechanics is HUMAN’s own open-source enforcers. The company publishes SDKs for Nginx, Node/Express, PHP, Python WSGI, and others under the PerimeterX GitHub org, and those repos contain the cookie parser the customer runs. They do not reveal how the backend decides the score; they do reveal exactly how the cookie is structured, decrypted, and verified, because the customer’s server has to do all three. Everything that follows about the byte layout comes from that published code, not from guesswork.
The wire format
A v3 cookie value is a small number of fields joined by colons. The enforcer splits the raw string on the colon character and reads it positionally. In the Python WSGI SDK the v3 cookie is split into up to four parts, and the structure is:
_px3 = hmac : salt : iterations : ciphertextThe first field is the HMAC signature, hex-encoded. The remaining three fields are the encrypted body: a base64 salt, an integer iteration count, and the base64 ciphertext. Compare that with a v1/v2 cookie, where the signature is not a separate leading field; the older format places the HMAC differently and the enforcer reconstructs it from the decrypted contents instead. The shape you actually see on the wire is the v3 one, and an example from the SDK’s own test fixtures looks like this (key field shortened):
bd078865...4593cd : OCIluokZ...a76C21A== : 1000 : zwT+Rht/...8vmA=Read left to right: a 64-hex-character HMAC, then the salt, then 1000, the PBKDF2 iteration count, then the AES ciphertext. The iteration count is not cosmetic. The enforcer validates it falls in a sane range (the Python SDK rejects anything outside 1 to 10000) before doing any cryptographic work, because an attacker who could set an absurd iteration count could otherwise turn cookie parsing into a denial-of-service knob.
Decrypting the body
The three encrypted fields drive a standard password-based decryption. The enforcer takes the customer’s cookie secret (a per-site key the customer holds, called cookie_key in the SDK config), runs PBKDF2-HMAC-SHA256 over it with the cookie’s salt and iteration count, and asks for 48 bytes of derived key material. It splits those 48 bytes: the first 32 become the AES-256 key, the last 16 become the CBC initialization vector. Then it decrypts the base64 ciphertext with AES in CBC mode and strips PKCS#7 padding. The result is a small JSON object.
In pseudocode, with no working secrets and no live values, the path is:
salt, iters, ct = split(cookie_body, ":")dk = PBKDF2_HMAC_SHA256(cookie_key, b64decode(salt), iters, dklen=48)key = dk[0:32] # AES-256 keyiv = dk[32:48] # CBC IVjson = unpad( AES_CBC_decrypt(key, iv, b64decode(ct)) )This is deliberately ordinary cryptography. There is nothing exotic in the decrypt step, and that is the point: the security does not live in obscurity of the algorithm, it lives in the secrecy of cookie_key and in the HMAC. The cookie_key is shared between HUMAN’s backend (which mints cookies) and the customer’s enforcer (which reads them). A client never has it. Without it the ciphertext is opaque, and even with a decrypted body in hand, the HMAC stops a client from editing the score and re-sealing the cookie.
A note on honesty here. The PBKDF2-SHA256, AES-256-CBC, 48-byte split, 32+16 key/IV layout is exactly what the open-source enforcers implement, so for the cookie the customer parses, this is documented, not inferred. What the backend does to produce the cookie, and the full set of inputs that feed the score, is not public. Where this post describes the issuance side, treat it as reconstructed from the validation side plus vendor docs, not as a leaked spec.
The fields inside
Once decrypted, the JSON is compact. The enforcers reference these keys directly:
tis the timestamp, the cookie’s expiry as a millisecond epoch value. The enforcer compares it against the current time; a cookie past itstis treated as expired regardless of anything else it says.sis the score. In the v3 enforcerssis read as the risk value the pass-or-block decision turns on. In older v1-style payloads the score lived under a sub-key (the SDK readss.b, a bot score, alongsides.a); in the v3 path the SDK readssas the score for the high-score test.ais the action, a single character telling the enforcer what to do for a blocked request. The PHP SDK maps these explicitly:cis captcha (the human challenge),bis a hard block,jis a JavaScript challenge,ris rate-limit.uis the UUID, a per-request or per-decision identifier used for logging and for correlating with HUMAN’s backend.vis the VID, the visitor ID, the same value carried in the_pxvidcookie.
So the decrypted _px3 is, in plain terms: here is a score, here is what to do if it is too high, here is who you are, and here is when this verdict stops being valid. The enforcer reads the score, compares it against the configured blocking threshold, and either passes the request or applies the action in a.
The HMAC, and why it knows your user agent
This is the part that catches people. The signature on a v3 cookie is not computed over the cookie body alone. The enforcer builds the string-to-sign by concatenating the raw cookie value with the request’s user-agent string, then runs HMAC-SHA256 over that with the secret, and compares the result against the hmac field in constant time. In the SDK the construction is literally raw_cookie + user_agent.
Bind the signature to the user agent and a cookie becomes non-portable across clients that present different UAs. A _px3 minted for one user-agent string fails its HMAC check the moment it is replayed under a different one, because the verifier folds the requesting UA into the digest and gets a mismatch. The v1 enforcer does something analogous but more elaborate: it rebuilds the base string from the decrypted fields in a fixed order (timestamp, then the score’s action and value, then UUID, then VID) and appends the user agent, checking variants with and without the client IP. v3 simplified this to “sign the whole raw cookie plus the UA,” which is both easier to get right and harder to partially forge.
The verification has four ways to fail, and the enforcers name each one in their pass-reason logging:
no_cookie: there is no_px3at all. The enforcer falls back to a server-to-server risk API call.cookie_decryption_failed: the body would not decrypt under the customer’scookie_key, usually a wrong key, a corrupted value, or a forged blob.cookie_validation_failed: the body decrypted but the HMAC did not match. This is the tamper signal, and it is also what fires when a cookie is replayed under a different user agent.cookie_expired: the body decrypted and the HMAC matched, buttis in the past.
In every one of those cases the enforcer does not simply block. It falls through to a synchronous server-to-server call to HUMAN’s risk API, sending request metadata and getting back a fresh verdict. The cookie is a fast path, not the only path. The SDKs even make that s2s call on some sensitive routes when the cookie is present and the score is low, because some decisions are worth the round trip regardless. The way the verdict ultimately turns into a number, and the signals that feed it, are covered in HUMAN’s collective signal network; the sensor payload and the press-and-hold challenge that produce a fresh cookie are in PerimeterX’s VID, sensor payload, and the bello challenge.
Where _pxvid and _pxhd fit
_px3 is the verdict. The other two cookies are identity and continuity, and they behave very differently from the risk cookie.
_pxvid is the visitor ID. It is a stable identifier, a UUID-shaped value, set as a persistent cookie so the same browser is recognizable across sessions and across the gaps between risk cookies. The same value appears as v inside the decrypted _px3. Its job is correlation: it lets the backend and the customer’s logs tie a sequence of requests, challenges, and verdicts to one visitor over time. It is not signed the way _px3 is and it is not the thing that grants access. On its own a _pxvid proves nothing about trust; it just says “this is the same browser I saw before.”
_pxhd is the harder one to pin down, because the public documentation is thin and the name is not officially expanded in the cookie docs. The enforcer source treats it as a HUMAN-set cookie with its own response-header bakery; the Nginx plugin even exposes a config flag for whether to set the Secure attribute “when baking a PXHD cookie,” and the Python middleware sets it with an explicit expires far in the future, which makes it persistent rather than session-scoped. Third-party cookie registries that catalogue it for privacy disclosures describe it as a long-lived HUMAN cookie, on the order of months to a year.
Read in context, _pxhd is best understood as a device or visitor health/history token: a persistent client-side breadcrumb that HUMAN sets and reads to carry continuity that should outlast any single _px3. The HD is widely read as “health/history data,” but that expansion is not something HUMAN states in the public cookie list, so treat it as the community’s inference rather than a vendor fact. What we can say with confidence from the enforcer code is narrower and more useful: _pxhd is HUMAN-set, persistent, baked back onto responses by the enforcer, and read alongside _px3 and _pxvid as part of the same request context. The exact internal field layout of _pxhd is not public; what circulates in reverse-engineering write-ups is inference from observed traffic, not a documented schema, and this post will not pretend otherwise.
The mobile twin: X-PX-Authorization
There is a fourth carrier worth naming because it confuses cookie-only analysis. On native mobile apps there is no cookie jar in the browser sense, so HUMAN’s mobile SDK presents the same risk token as a request header, X-PX-Authorization, instead of as _px3. The enforcer’s context object tracks whether the token arrived as a cookie or as a header (the PHP SDK records a cookie_origin of either cookie or header) and parses it the same way. The version prefix that distinguishes v1 from v3 applies to the header form too. So when a write-up talks about the _px3/_pxvid/_pxhd triple “plus the X-PX-Authorization header,” that header is not a separate signal: on mobile it is the risk token, playing the role _px3 plays on the web.
What the signing buys, and what it does not
The HMAC plus the user-agent binding plus the short expiry together make a _px3 cheap to verify and expensive to abuse. A client cannot read the score without the customer key. A client cannot raise its own score, or flip the action from block to pass, without breaking the HMAC. A client cannot lift a good cookie from one browser and replay it under a different user agent, because the UA is folded into the digest. And a stale cookie eventually trips the t check and forces a fresh decision through the risk API.
What the signing does not do is make the cookie the whole defense. The cookie only carries a verdict the backend already reached, and that verdict is only as good as the signals behind it. This is the same shape every modern anti-bot vendor has converged on: a small signed token at the cookie layer fronting a much larger scoring system that the token never exposes. Akamai’s _abck and DataDome’s datadome cookie solve the same problem with different crypto and a different field layout; the comparison is in Akamai Bot Manager’s _abck cookie and the DataDome cookie lifecycle. The behavioral-biometrics idea underneath all of them, that the way an input device is used can be classified as human or not, is old enough to have its own patent literature; US 11,003,748 B2, Utilizing behavioral features to identify bot (Unbotify Ltd., issued May 2021), lays out the two-classifier approach of deciding human-versus-bot and then original-versus-replayed from mouse and timing features. That patent is not PerimeterX’s, but it describes the class of signal that feeds a score like the one s carries.
The thing to hold onto is that _px3 is small on purpose. It is four colon-separated fields, one of them a signature, wrapping a JSON object of maybe five keys. All the weight is in the key the client never sees and the score the client can never edit. Decrypt one and you learn what HUMAN decided about a request; you learn almost nothing about how. That asymmetry is the whole design, and it is why reading the cookie is easy and forging one is not.
Sources & further reading
- HUMAN Security (2024), Use of cookies & web storage — the vendor’s own list of
_px,_px2,_px3,_pxhd,_pxvidand the v2 vs v3 Risk Cookie distinction. - PerimeterX (2024), perimeterx-python-wsgi: px_cookie.py — the cookie parser:
salt:iterations:ciphertext, PBKDF2-SHA256, AES-256-CBC, the 48-byte key/IV split. - PerimeterX (2024), perimeterx-python-wsgi: px_cookie_v3.py — the v3 HMAC base string built as
raw_cookie + user_agent, and thet,s,a,u,vfields. - PerimeterX (2024), perimeterx-python-wsgi: test_px_cookie_validator.py — fixtures showing the four-field cookie and the failure reasons (
no_cookie,cookie_decryption_failed,cookie_validation_failed,cookie_expired). - PerimeterX (2024), perimeterx-php-sdk: PerimeterxContext.php — the action codes (
ccaptcha,bblock,jchallenge,rratelimit), the cookie-vs-header origin, andX-PX-Authorization. - PerimeterX (2024), perimeterx-nginx-plugin: README — the
blocking_scorethreshold, the server-to-server fallback, and the PXHDSecure-flag bake option. - PerimeterX, Inc. (2020), US Patent 10,708,287 B2: Analyzing client application behavior to detect anomalies and prevent access — the security verification system that issues a token to the user-agent after running tests.
- PerimeterX, Inc. (2021), US Patent 10,951,627 B2: Securing ordered resource access — the risk token carrying a score and expiration, consulted by secondary servers.
- Unbotify Ltd. (2021), US Patent 11,003,748 B2: Utilizing behavioral features to identify bot — the two-classifier behavioral-biometric approach (human-vs-bot, then original-vs-replayed) underlying scores of this kind.
- Trickster Dev (2023), How does PerimeterX Bot Defender work — a researcher’s overview of the Security Loader, Security Module, and the risk-token-to-secondary-server flow.
- HUMAN Security (2022), HUMAN and PerimeterX unite in market-changing merger — the 2022 merger that put the PerimeterX
_pxplumbing under the HUMAN brand.
Further reading
HUMAN's collective signal network: how cross-customer telemetry feeds detection
Traces how HUMAN Security aggregates signals across its customer base, from its White Ops ad-fraud heritage to the Satori threat-intel disruptions, and what the collective-defense model can and cannot see.
·19 min readThe DataDome cookie lifecycle: token issuance, rotation, and validation
Traces the datadome cookie end to end: how it is issued after a challenge, what the 128-byte token encodes, when it rotates, how long it lives, and how the edge validates it on every request through the Protection API.
·22 min readAkamai's bm_sz cookie and pixel challenge: the second layer most clients miss
Traces the bm_sz cookie, the pixel challenge that mints ak_bmsc, and the sec-cpt proof-of-work interstitial that sits alongside _abck, and why a client that only validates _abck still gets challenged or dropped.
·22 min read