PerimeterX's VID, sensor payload, and the bello challenge internals
Load a site behind PerimeterX, open the network tab, and you will see a POST to a path that ends in /api/v2/collector. The body is a short JSON object. One of its keys holds a long base64 string. Decode that string and you get more JSON, except the keys are not words. They are codes: PX315, PX320, PX340, PX257. The string is signed. The script that built it renamed every one of its own functions the moment the page loaded, and it will rename them again on the next refresh. That collector script has a name in the reverse-engineering community. People call it bello, after a token that shows up in the served file. It is the client half of a system that decides whether you get a valid _px3 cookie or a press-and-hold challenge.
This post is about that client half and the identity that ties it together. Not the server-side risk scoring, not the full _px3 and PXHD cookie validation handshake, which has its own post. The question here is narrower. What is the VID, where does it live, and how does it follow a visitor across sessions? What is inside the sensor payload the bello script POSTs, and how is the whole thing obfuscated so that reading the script tells you almost nothing? And when detection trips, what does the press-and-hold challenge actually measure and send back? The exact field layout is not published by PerimeterX, and the script is rebuilt on every load, so where the detail comes from observed traffic and reverse-engineering rather than vendor documentation, this post says so.
The walk goes like this. First the VID and the cookie identity chain it belongs to. Then where the bello sensor comes from and how it reaches the collector. Then the payload itself: the PX-numbered field grammar, what the markers carry, and what stays opaque. After that the obfuscation, the per-load renaming and VM-style protection that make a static read useless. Then the challenge flow, the press-and-hold biometrics, and the signed solution the client returns. Finally what the mobile SDK does differently, and what the whole design says about where PerimeterX puts its trust.
A note on names. PerimeterX merged with HUMAN Security on 27 July 2022, and the combined company now operates under the HUMAN name. The client artifacts still carry the PX prefix everywhere, so this post uses “PerimeterX” for the technology and notes the rebrand where it matters.
The VID and the identity chain
PerimeterX does not rely on a single token. It chains several, and each one does a different job. The visitor identifier, the VID, is the one that persists. It lives in a cookie named _pxvid, and PerimeterX’s own integrations confirm the mapping: the server-side PHP SDK reads _pxvid straight into a context field it calls $this->vid, exposed through a getPxVidCookie() accessor. The VID is a UUID-shaped value assigned to a browser to track it as one visitor across requests and across sessions. Captain Compliance’s writeup on the cookie puts its lifetime at one year, which matches a tracking identifier rather than a per-session token.
That makes the VID the slow-moving anchor in a set of cookies that move at different speeds. The _px3 cookie is the security token, short-lived, refreshed by the collector on each successful evaluation. The _pxvid cookie is the long-lived visitor anchor. Alongside them sit _pxhd, a device-and-history hash the SDK reads into a getPxhdCookie() accessor, and pxcts, a cross-tab session value. On the mobile side the equivalent of the cookie token rides in headers instead: X-PX-Authorization carries the token, and X-PX-Original-Token carries the original where one applies, both read by the same PHP SDK.
The reason the VID matters more than its single-cookie footprint suggests is what it lets the server do. A request without a VID is a first-time visitor, evaluated cold. A request with a VID that has built up a history of clean evaluations is a known-good browser, and the bar for it drops. A VID that has been seen across an implausible spread of IPs, or paired with mismatched fingerprints, is the opposite. The VID is the handle the server-side model grabs to connect today’s request to the same browser’s behavior last week. The collector payload is what keeps feeding that handle new evidence.
It helps to be precise about what the VID is not. It is not a secret, and it is not a proof of anything. Anyone can read their own _pxvid, copy it, clear it, or hand it to a different client. PerimeterX assumes all of that. The value of the VID to the server is purely as a join key. It groups requests that claim to be the same browser so the model can ask whether that claim holds up against everything else: the same TLS fingerprint each time, a plausible geographic path across IPs, behavioral telemetry that stays consistent with one human. A VID copied onto a thousand machines becomes a liability for whoever copied it, because it now ties a thousand inconsistent sessions to one identity, which is exactly the pattern the model is built to surface. The persistence cuts both ways, and that is deliberate. A long-lived anchor rewards a stable, honest browser and penalizes a reused one.
This is also why clearing cookies is a weaker move against PerimeterX than it looks. Dropping the _pxvid does not return you to neutral. It returns you to the cold-start bucket, evaluated without history, which on a well-tuned deployment is a more suspicious place to be than a browser carrying a clean record. The VID is one input the server can recover from other signals anyway: the device-and-history hash in _pxhd, the TLS and HTTP/2 fingerprints, the IP. Losing the cookie loses the convenient handle, not the underlying ability to recognize a returning client.
Where the bello sensor comes from
The collector is a single obfuscated JavaScript file. In a standard third-party deployment the browser fetches it from a PerimeterX-controlled host, and the integration constants in PerimeterX’s own NGINX plugin name the domains involved: perimeterx.net for collectors, px-cloud.net and px-cdn.net for CDN-served assets and captcha. The collector endpoint itself is a path ending in /api/v2/collector, confirmed both by community traffic captures against sites like Fiverr (which POST to <appId>.px-cdn.net/api/v2/collector) and by the mobile collector path in the same family.
Many large sites run PerimeterX in first-party mode, which changes the hostnames but not the mechanics. In that setup the edge is configured to serve the script and proxy the collector under the site’s own domain. PerimeterX’s CDN integration docs spell out the rewrite: an incoming request to https://www.customerdomain.com/<app_id without PX>/xhr/... is mapped to an origin call against collector-<app_id>.perimeterx.net/..., and the script itself is served from a first-party /init.js-style path with /xhr and /captcha siblings. To a browser it all looks same-origin, which is the point: it sidesteps third-party cookie restrictions and makes the traffic harder to block with a simple host rule.
However it is served, the script does two kinds of work once it runs. It probes the browser environment once at startup, and it hooks event listeners that accumulate behavioral data over the life of the page. PerimeterX’s product material for Bot Defender describes the client as an obfuscated module that gathers a large set of device, browser, and behavioral signals, encrypts them, and sends them to the collector to obtain the security token. The community count that gets repeated is on the order of a hundred-plus signals, covering canvas and WebGL rendering, AudioContext, navigator and screen properties, plugin and font enumeration, WebRTC, and the full mouse, scroll, and keystroke stream. The exact set is not published and varies by configuration.
*The collector POST is the hinge. The same payload either renews the token and the VID's clean history, or routes the visitor into the press-and-hold challenge.*The transport is a POST whose JSON body wraps the encoded sensor string. Community reversing of the request lists the fields that ride alongside the payload: an appId (the site key, prefixed PX), a tag that encodes the script version for that site, a uuid request identifier, and counters like rsc (a request count) and seq_rsc. Many of these are stable per site or per session. The interesting one, the part that carries the actual telemetry, is the base64 payload field.
Inside the payload: the PX field grammar
Decode the base64 payload and you get JSON whose keys are PX-prefixed codes rather than English. This grammar is clearest in the mobile SDK, where a researcher’s teardown of the iOS collector documented the outer shape directly. The payload is an object with a type tag t set to a value like PX315, and a data object d whose keys are individual PX codes. From that teardown, the device-and-environment block maps roughly like this: PX320 is the device model, PX321 the device name, PX322 the platform, PX318 the OS version, PX323 a timestamp, PX326 a session UUID, PX327 a short identifier, PX328 an integrity hash (described as a SHA-1 used for tamper detection), PX340 the SDK version, PX341 the application name, and PX348 the bundle identifier.
The browser payload uses the same PX-numbered convention but a different and configuration-dependent set of codes, and that set is not publicly documented as a stable table. What community work has pinned down is specific codes that show up in the challenge path rather than the whole environment map. One writeup on a recent browser script version (cited as 8.7.2) names a key PX11590 for the second payload and an initiated key PX12573 whose computed result must be passed through Math.floor. Those are real observed codes for that version, but they are version-specific. The honest statement is that the browser payload field layout is not public, it differs across the script versions PerimeterX rolls out, and any fixed mapping you see published is a snapshot of one version at one time.
What unifies both is the integrity layer. The payload is not just collected data, it is collected data plus a value that proves the right script produced it. In the mobile teardown that is PX328, a hash over inputs the server can recompute. In the browser, the equivalent is the signing the product docs allude to when they describe the telemetry as encrypted before it leaves. The practical effect is that a server can reject a payload whose fields look plausible but whose signature does not match what its own copy of the algorithm would generate for those fields. That is the lever that turns “replay yesterday’s payload” from a shortcut into a dead end.
Obfuscation: why a static read tells you nothing
The bello script is built to resist exactly the kind of analysis this post describes. Two techniques do most of the work, and they compound.
The first is heavy obfuscation. A deobfuscated browser script runs to thousands of lines, with one reversing project putting a recent version (8.9.6) at over 9,000 lines once the polyfills and helper functions are expanded. Strings are encoded, control flow is flattened, and the logic that matters is buried among decoys. That alone is standard for anti-bot collectors. The second technique is the one that hurts: the names change on every load. The same project describes it plainly, that with the VM-style protection in place, “every time you refresh the page the function and variable names will change.” So a note you take on this load (function a7Q builds the canvas hash) is worthless on the next, because a7Q no longer exists and the canvas logic now lives somewhere else under a new name.
That combination is why a static read of the file is close to useless and why the work is done dynamically instead, by watching what the script does at runtime rather than what it says. It also explains why published field maps go stale so fast. They are not just describing a secret, they are describing a moving target, and PerimeterX moves it on a schedule.
*The logic is identical between loads; only the identifiers rotate. A name-based note taken on one load does not survive the next, which is what pushes analysis from static reading to runtime observation.*There is a reason for this design beyond raw difficulty. PerimeterX wants the cost of keeping a solver working to be continuous, not one-time. A static obfuscator you crack once and you are done. A per-load VM means whoever is solving has to maintain a deobfuscation and tracing pipeline that survives every script update PerimeterX ships, and PerimeterX ships them often. The defender’s cost is a one-time engineering investment in the build pipeline. The attacker’s cost recurs forever. That asymmetry is the actual product.
It is worth being clear about what the renaming does and does not protect, because it is easy to overstate. The renaming does not hide the algorithm in any information-theoretic sense. The same canvas-hashing routine, the same signing step, the same event hooks exist on every load. A patient analyst who instruments the runtime can find them each time. What the renaming defeats is the cheap, durable artifact: a map. You cannot write down “the signing function is called _0xd” and reuse it, because that name is gone next load. So the knowledge that survives across loads has to be expressed in terms that do not depend on names, which is to say in terms of behavior, call patterns, and the structure of what the script touches in the DOM and the global scope. That is harder to capture, slower to build, and far more fragile to maintain. The VM-style protection turns a one-page cheat sheet into a standing engineering commitment.
The polyfills bundled into the script are part of the same strategy in a quieter way. A collector that ships its own implementations of common primitives instead of calling the browser’s built-ins is one that can detect tampering with those built-ins, and one that behaves consistently across the long tail of browsers it has to fingerprint. It also pads the script with thousands of lines of plausible, working code that has nothing to do with the parts an analyst cares about, which raises the cost of finding the parts that matter. None of this is unique to PerimeterX; the same playbook shows up in the Akamai sensor collector and in DataDome’s JS tag. What distinguishes PerimeterX is how aggressively it leans on the per-load rotation as the centerpiece rather than as a finishing touch.
The challenge: press, hold, and prove it
When the server is not satisfied (a missing or stale VID history, a fingerprint mismatch, a payload that does not sign cleanly, a bad IP reputation) it routes the visitor into a challenge instead of issuing a token. The modern PerimeterX challenge is the press-and-hold prompt, marketed under the HUMAN brand as a frictionless alternative to image puzzles. The user presses a button and holds it for a moment. No grid of crosswalks. The interaction looks trivial. The measurement is not.
What the challenge measures during that hold is behavioral biometrics. PerimeterX builds a real-time profile from the micro-signals of the press: the timing and frequency of pointer events during the hold, the small involuntary jitter in a human’s hold versus the dead-still flatness of a scripted one, and the cadence of the animation-frame loop while the button is down. Coverage of the mechanism describes it as watching the frequency of mousedown-style events and requestAnimationFrame ticks to separate organic human noise from a robotic static hold. A genuine hold is never perfectly still. A naive automation of it is. The model is trained to tell those apart.
Pressing the button is the easy part. The hard part is the payload that goes with it. The press-and-hold UI is a front end on the same collector machinery, and completing the challenge means emitting a fresh, valid, signed telemetry payload that carries the recorded motion profile and matches everything else the server already knows: the TLS fingerprint of the connection, the VID’s history, the HTTP/2 frame characteristics. Scrapfly’s writeup frames the bar precisely, that the easy part is pressing the button and the hard part is “the collector script emitting a valid telemetry payload,” with real human motion profiles and matched collector output. All the layers have to line up. A perfect motion recording attached to a payload that fails the signature check, or that arrives over a TLS fingerprint that does not match a real browser, does not pass.
The mobile challenge makes the “prove the right code ran” requirement explicit and computational. The iOS teardown describes the server returning, inside the challenge response, a do array of operators that encode a small program: a sequence like challenge|type|PX259|PX256|op1...op6 with six math operators. The client has to run those operators to compute an integer and return it in a field called PX257. On iOS the operators index a dispatch table inside the native binary, so the answer can only be produced by running PerimeterX’s own code path. It is a proof-of-execution: not “can you do arithmetic” but “did our exact challenge logic run in your client.” The browser challenge is the same idea in JavaScript, where, as noted above, an observed version required flooring a computed value with Math.floor before returning it.
This is the part of the system that resists a clean-room reimplementation hardest. The behavioral measurement can in principle be fed synthetic-but-realistic motion. The signature can in principle be recomputed if you have ported the algorithm. The proof-of-execution wants the actual challenge code to have run, and on mobile that code is compiled ARM with stripped symbols, while in the browser it is the per-load VM. Both make “just port the function” a continuously decaying investment.
The mobile SDK: same idea, harder shell
PerimeterX’s mobile SDK solves the same problem with different machinery, and the difference is instructive. In the browser, the logic is JavaScript, hostile but ultimately readable at runtime. In the iOS SDK, the equivalent logic is compiled into the app binary. The teardown that documented the PX field codes describes roughly two megabytes of ARM64 code with stripped symbols, where the challenge computation happens in native instructions rather than in any interpretable script. The dynamic dispatch through a lookup table at a fixed binary address is the native analogue of the browser VM: server-sent values select which native function runs.
The payload story is the same one in a tougher wrapper. The mobile collector hits paths like /api/v1/collector/mobile and /api/v2/collector, the token rides in X-PX-Authorization rather than in a cookie, and the PX-keyed payload carries the device fingerprint with PX328 as its integrity hash. The merger that produced HUMAN folded PerimeterX’s mobile protection into the broader platform, and the collective signal work that feeds detection draws on both web and app telemetry. The client artifacts kept their PX naming through all of it.
The takeaway from the mobile side is that PerimeterX treats the client as something it can make expensive to forge but never trusts on its own. A native binary is harder to read than a script, but it still runs on a device the operator controls. So the design does not bet everything on the shell being unbreakable. It bets on the combination: the shell raises the cost, the signature ties the payload to genuine collection, the VID ties this session to a history, and the server-side model judges the whole bundle against everything else it can measure on the wire.
What the client design actually tells you
Strip away the field codes and the obfuscation tricks and the architecture is simple to state. PerimeterX puts a persistent identity in the browser (the VID), feeds it a continuous stream of signed telemetry from a collector that is rebuilt on every load (bello), and when the stream is not convincing, demands a proof-of-execution wrapped in a behavioral challenge (press-and-hold). No single piece is meant to be unbreakable. The VID can be cleared. The payload can be studied at runtime. The challenge math can be ported. What is meant to be unbreakable is keeping all of them aligned at once, indefinitely, against a script and a binary that change underneath you.
That is why the most durable detail in this whole post is not any PX code, since those rotate, but the per-load renaming and the proof-of-execution. Those are not facts about a payload, they are facts about cost. The PX field numbers in a published teardown are a snapshot, true for the version someone happened to capture and stale by the next deploy. The honest reference value here is structural: a VID that persists for about a year as the server’s handle on a browser, a signed PX-keyed sensor payload that has to match the TLS and HTTP/2 layers around it, and a challenge that wants the genuine code to have run, not merely the right answer. Quote the structure. Treat any fixed field map as a date-stamped observation, because PerimeterX wrote it to expire.
Sources & further reading
- HUMAN Security (2025), Use of cookies & web storage — vendor documentation listing the PX cookie set and its purposes (now under the humansecurity.com docs domain).
- PerimeterX (2024), perimeterx-php-sdk: PerimeterxContext.php — official server SDK source showing
_px3,_pxvid,_pxhd,pxcts, the VID mapping, and theX-PX-Authorization/X-PX-Original-Tokenheaders. - PerimeterX (2024), perimeterx-nginx-plugin: pxconstants.lua — official integration constants naming the collector, captcha, and risk endpoints and the perimeterx.net / px-cloud.net domains.
- HUMAN Security (2024), Supporting first-party HUMAN calls on a CDN — vendor docs describing the first-party rewrite from a site path to the
collector-<app_id>.perimeterx.netorigin. - Biplov Dahal (2024), Breaking Mobile Bot Protection: Reverse Engineering PerimeterX’s iOS SDK — the teardown that documents the PX-keyed payload (
PX315,PX320,PX328,PX340), thedooperator array, and thePX257solution. - glizzykingdreko (2024), Extracting PerimeterX’s Dynamic Functions for Advanced Anti-Bot Evasion — researcher write-up naming version-specific browser keys (
PX11590,PX12573) and theMath.floorstep in a challenge computation. - Pr0t0ns (2025), PerimeterX-Reverse — reversing project README documenting the VM-style per-load renaming, the ~9,000-line deobfuscated size for v8.9.6, and the collector request fields (
appId,tag,uuid,rsc,seq_rsc). - Scrapfly (2026), Bypass PerimeterX / HUMAN Security — vendor-neutral overview of the cookie chain, the signed telemetry requirement, and the press-and-hold challenge’s collector-payload bar.
- Captain Compliance (2024), pxvid Cookie: What is it & How Does it Work? — plain-language reference giving the
_pxvidone-year lifetime and visitor-tracking role. - TechCrunch (2022), Human Security merges with PerimeterX — contemporaneous report on the 27 July 2022 merger and the move to the HUMAN brand.
- ScrapingBee (2026), How to bypass PerimeterX anti-bot protection system in 2026 — overview of the multi-layer detection model and the
pxhd/px.js/perimeterx.netfingerprints of a PerimeterX deployment. - HUMAN Security (2024), Bot Defender / Code Defender product pages — vendor description of the obfuscated client module that collects device, browser, and behavioral signals and encrypts them before sending.
Further reading
Akamai's sensor_data payload: the fields and their telemetry sources
A field-by-field tour of the sensor_data payload Akamai Bot Manager POSTs from the browser: what telemetry it carries, how the numeric field markers are laid out, and how the obfuscation and encryption have changed across v1, v2, and v3.
·19 min readPerimeterX to HUMAN: the rebrand and what changed under the hood
Traces how PerimeterX became part of HUMAN Security through the July 2022 merger, the White Ops lineage behind that name, and which parts of the bot-detection stack actually changed versus which were only relabelled.
·19 min readHUMAN's _px3 cookie and the PXHD flow explained
Traces HUMAN's _px3 risk cookie end to end: the salt:iterations:ciphertext wire format, the AES-CBC and PBKDF2 layer, the HMAC bound to the user agent, the score and action fields, and how _pxhd and _pxvid sit alongside it.
·17 min read