By the end of this guide you will be able to confirm that a site runs PerimeterX, find the values the task needs, get the _px3 cookies from AntiPerimeterXTask, and replay them from the connection that earned them. It also covers how to tell which layer of the stack is blocking you when a 403 keeps coming back.
What PerimeterX checks
PerimeterX is now part of HUMAN Security, but the cookies, endpoints and most documentation still say PerimeterX or PX. The product in front of scrapers is Bot Defender: a JavaScript sensor plus a cloud risk score per request, with models trained for each customer. Enforcement is a block page, or a 403 that carries a PX reference id.
Reports on how PX scores a request list these layers: TLS, HTTP/2 and header fingerprints, IP reputation, the JavaScript sensor, behaviour and the per-customer model. The fingerprints are scored before any challenge is shown. That is why a correct solve can still fail when it is replayed from the wrong client or the wrong address. The PerimeterX solver page has a shorter overview.
Invisible, press-and-hold or mobile
| Surface | What the visitor sees | What a solve produces |
|---|---|---|
| Invisible sensor | Nothing; silent scoring | _px3 cookie |
| Press and hold | A "Press & Hold to confirm you are human" page, sometimes a slide or image-match variant | _px3 cookie |
| Mobile app | Nothing in a browser | Reportedly an x-px-authorization header set, not cookies |
SurSolver has one task type for PerimeterX, AntiPerimeterXTask, and its result holds cookies, token and userAgent. No header output is documented, so do not plan around mobile app traffic.
The task accepts blockData, blockMode and initCookies as optional fields. Other providers' documentation uses the same names for their press-and-hold flow: blockData is the raw body of the 403 you fetched through your proxy, initCookies the cookies of that response, and blockMode the shape of the block (for example xhr). SurSolver's reference lists the names but does not say what they change, so test them on your target before relying on them.
Confirm that a site runs PerimeterX
Page source and cookies take a minute to check; the Network panel takes a few more.
| Marker | Where you see it |
|---|---|
_px3 or _px2 cookie on first load |
Application → Cookies |
_pxvid, sometimes _pxhd |
Application → Cookies |
_pxAppId: PX followed by eight characters |
Page source, window._pxAppId |
//client.perimeterx.net/<id>/main.min.js |
Script tag in the page source |
Requests to *.px-cloud.net, *.px-cdn.net, *.perimeterx.net |
Network panel |
An unexplained POST on every page, or an init.js on a random path of the site's own domain such as /px/PXxxxxxxxx/init.js |
Network panel; first-party deployments hide the PX hosts |
px-captcha, captcha.px-cdn.net, "Please verify you are a human", "Access to this page has been denied" |
Body of a 403 or 429 |
An iframe on iframe.hsprotect.net or *.px-cdn.net |
The challenge itself; never use its URL as websiteURL |
This function checks a response against the markers. Feed it the status, the Set-Cookie headers and the body of a request you made through your proxy. Page source varies by site, so confirm any app id it prints in DevTools.
const PX_BODY_MARKERS = [
'_pxAppId',
'px-captcha',
'captcha.px-cdn.net',
'perimeterx',
'Please verify you are a human',
'Access to this page has been denied',
];
const PX_COOKIE_NAMES = ['_px3', '_px2', '_pxhd', '_pxvid'];
const BLOCK_STATUSES = [403, 429];
const APP_ID_PATTERNS = [
/_pxAppId['"]?\s*[:=]\s*['"](PX[0-9A-Za-z]{8})['"]/,
/client\.perimeterx\.net\/(PX[0-9A-Za-z]{8})\/main\.min\.js/,
/\/(PX[0-9A-Za-z]{8})\/init\.js/,
];
export function inspectPerimeterX(status, setCookieHeaders, body) {
const lowerBody = body.toLowerCase();
const markers = PX_BODY_MARKERS.filter((m) => lowerBody.includes(m.toLowerCase()));
const cookies = PX_COOKIE_NAMES.filter((name) =>
setCookieHeaders.some((header) => header.startsWith(`${name}=`)),
);
const appId = APP_ID_PATTERNS.map((pattern) => body.match(pattern)?.[1]).find(Boolean) ?? null;
return {
blocked: BLOCK_STATUSES.includes(status) && markers.length > 0,
markers,
cookies,
appId,
};
}
The cookies you get back
| Cookie | Role |
|---|---|
_px3 |
The main clearance cookie; newest and highest priority |
_px2 |
The older equivalent, used when there is no _px3 |
_pxhd |
A hardened-device cookie; one provider reports it lasting up to 24 hours |
_pxvid |
The visitor id; send it as uuid |
The exact names vary by target. Replay everything in solution.cookies, not only _px3. When token is present it carries the main cookie value, but cookies is the full set.
Lifetimes are reported inconsistently: about 60 seconds for _px3 on newer high-security deployments, up to an hour on others. Neither figure is guaranteed, so cache a working set and re-solve when you are blocked again.
Find the parameters
| Field | Required | Where to find it |
|---|---|---|
websiteURL |
Yes | The parent page URL in the address bar, never the iframe URL |
pxAppId |
Yes | _pxAppId in page source, or the id in the main.min.js URL |
| proxy fields | Yes | Your sticky proxy; there is no proxyless form |
uuid |
No | The _pxvid cookie value |
vid |
No | window._pxhc.vid in the console; it often equals uuid |
userAgent |
No | The user agent of the session you will replay with |
Pick the proxy now. Residential, mobile or static ISP addresses pass the IP layer; datacenter addresses fail it. Keep the session sticky for roughly 5 to 10 minutes so fetch, solve and replay share one IP. The proxy guide lists the accepted formats, and proxies for captcha solving covers choosing a class.
Solve it and replay the cookies
This script creates the task, polls every 3 seconds and loads the page in Playwright with the returned cookies, the returned user agent and the same proxy. Replace the placeholders with your own values; delete the uuid line if you have no _pxvid.
import { chromium } from 'playwright';
const API = 'https://api.sursolver.com';
const CLIENT_KEY = 'YOUR_API_KEY';
const PAGE_URL = 'https://www.example.com/products';
const PX_APP_ID = 'YOUR_PX_APP_ID';
const PXVID = 'YOUR_PXVID_COOKIE_VALUE';
const USER_AGENT = 'YOUR_BROWSER_USER_AGENT';
const PROXY = {
host: 'proxy.example.com',
port: 8000,
login: 'YOUR_PROXY_LOGIN',
password: 'YOUR_PROXY_PASSWORD',
};
const POLL_MS = 3000;
const POLL_ATTEMPTS = 60;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function call(path, body) {
const res = await fetch(`${API}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
// API errors are HTTP 200 with errorId 1; anything else is a transport failure.
if (!res.ok) throw new Error(`HTTP ${res.status} from ${path}`);
const data = await res.json();
if (data.errorId !== 0) throw new Error(`${data.errorCode}: ${data.errorDescription}`);
return data;
}
async function solvePerimeterX() {
const created = await call('/createTask', {
clientKey: CLIENT_KEY,
task: {
type: 'AntiPerimeterXTask',
websiteURL: PAGE_URL,
pxAppId: PX_APP_ID,
uuid: PXVID,
userAgent: USER_AGENT,
proxyType: 'http',
proxyAddress: PROXY.host,
proxyPort: PROXY.port,
proxyLogin: PROXY.login,
proxyPassword: PROXY.password,
},
});
for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt += 1) {
const result = await call('/getTaskResult', {
clientKey: CLIENT_KEY,
taskId: created.taskId,
});
if (result.status === 'ready') return result.solution;
await sleep(POLL_MS);
}
throw new Error(`task ${created.taskId} was not ready in ${POLL_ATTEMPTS} polls`);
}
async function main() {
const solution = await solvePerimeterX();
const browser = await chromium.launch({
proxy: {
server: `http://${PROXY.host}:${PROXY.port}`,
username: PROXY.login,
password: PROXY.password,
},
});
try {
// solution.userAgent is present only when the solver reports one.
const context = await browser.newContext({ userAgent: solution.userAgent ?? USER_AGENT });
const { hostname } = new URL(PAGE_URL);
await context.addCookies(
solution.cookies.map((cookie) => ({
name: cookie.name,
value: cookie.value,
domain: cookie.domain || hostname,
path: cookie.path || '/',
})),
);
const page = await context.newPage();
const response = await page.goto(PAGE_URL);
console.log('status:', response.status());
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Check the content, not only the status: a hold that the challenge UI accepted does not mean the site's risk check passed, so wait for the protected content before you treat the session as cleared. With Selenium instead of Playwright the same cookies and user agent apply; see Selenium captcha solving.
Stay cleared
- IP binding is strict. Fetch, solve and replay go through the same proxy; a different IP, user agent or proxy gives a 403 even with a valid cookie.
- Send the user agent from the solution exactly when one is returned. Otherwise reuse the one you triggered the block with. See browser headers and user agents.
- PX trains per-site models. A result that works on one app id may fail on another, so test on the real deployment.
- Each accepted task is charged and a task that finishes as failed automatically returns its original charge once, so re-solving in a loop on a bad proxy wastes time without clearing anything (billing).
Which layer is blocking you
| What you observe | Layer | Does a new solve help? | What to do |
|---|---|---|---|
403 block page and no _px3 yet |
Challenge | Yes | Send AntiPerimeterXTask. Akamai block pages look similar but return cookies instead — Akamai solver API |
| "Press & Hold" page in your own automation | Behaviour | No | Do not click or navigate during the hold; it reads mouse movement and hold timing. The button often sits in a closed Shadow DOM, so Selenium locators miss it. Send the task to the API instead |
| 403 even though the cookie is valid | IP or user agent consistency | Only from the same IP | Replay through the proxy and user agent of the solve |
| The captcha keeps coming back | IP reputation | No | Move to a cleaner IP range; this is a proxy problem |
| 429 | The site's own rate limit | No | Slow down or widen the pool of addresses |
requests-style client is blocked before any challenge |
TLS, HTTP/2, headers | No | Use a client whose fingerprint and header order match a real browser |
ERROR_FAILED from the API |
Solve failed | Submit a new task | The task was charged; check proxy quality and websiteURL |
ERROR_INVALID_REQUEST |
Request shape | After correcting it | The description names the field; nothing was charged |
Next steps
Open the target in your own proxy, confirm the markers and copy the app id and the parent page URL. Then send them with AntiPerimeterXTask in one createTask call. The accepted parameters are in the public catalog (GET /v1/task-types), and the statuses and failure handling are in the task lifecycle guide. For DataDome, which returns a datadome cookie instead, see DataDome solver API.