If you’ve ever tried to automate data scraping from a mobile app, chances are you’ve run into PerimeterX (now HUMAN). And if you have, then you know just how tough it can be to get around. Especially in its mobile form, this protection system has become one of the most advanced and frustrating obstacles developers face today.
That was exactly my situation—until I spent several months reverse-engineering how PerimeterX Mobile works. With a lot of testing, iteration, and more dead ends than I can count, I finally figured out a method that bumped my success rate from 20% to consistently over 90%. Apps like StockX, TextNow, and GrubHub? I can now extract data from them automatically without getting flagged.
In this guide, I’ll walk you through everything I’ve learned: how to identify PerimeterX, understand its token structure, generate valid tokens, and implement them in your requests. Plus, I’ll share some practical tips for keeping your sessions alive and handling common roadblocks along the way.
What is PerimeterX Mobile?
PerimeterX, recently rebranded as HUMAN, is a powerful bot mitigation solution designed specifically to protect mobile apps and APIs from automated abuse. Unlike its web counterpart, PerimeterX Mobile is tailored for mobile platforms like iOS and Android.
You’ll usually encounter one of two versions—px2 or px3. Px3 is the newer, more robust version and, not surprisingly, the harder one to crack. The protection relies on a mix of device fingerprinting, behavioral data, and token validation to determine whether a request is legitimate.
The short version? If you’re not sending the right tokens in your API calls, your requests are going to get blocked—fast.
Step 1: Identify PerimeterX Implementation
Before you even think about bypassing PerimeterX, you first need to confirm that it’s actually in play—and figure out which version you’re dealing with. That means digging into your network traffic and identifying key indicators in the headers and URLs.
import requests
import re
def detect_perimeterx(request_headers, response_headers, url):
# Check request headers for PX tokens
px_headers = [
'x-px-authorization',
'x-px-original-token',
'x-px-context'
]
for header in px_headers:
if header in request_headers:
# Check for PX3 format
if re.match(r'3:[a-f0-9]+:[A-Za-z0-9+/]+={0,2}:\d+:[^:]+', request_headers[header]):
print("PerimeterX px3 detected in request headers")
return "px3"
# Check for PX2 format
if re.match(r'2:[A-Za-z0-9+/]+={0,2}', request_headers[header]):
print("PerimeterX px2 detected in request headers")
return "px2"
# Check for collector endpoints in the URL
px_endpoints = [
'collector-[a-zA-Z0-9]+\.perimeterx\.net',
'collector-[a-zA-Z0-9]+\.px-cloud\.net'
]
for pattern in px_endpoints:
if re.search(pattern, url):
print("PerimeterX collector endpoint detected in URL")
return "unknown_version"
return None
# Example usage with mitmproxy or other traffic capture
headers = {
"x-px-authorization": "3:5b0de8f7a1d99c67add:MOXOASOAP5VNA==:1000:6xXzN9Q="
}
url = "https://api.stockx.com/v2/products"
px_version = detect_perimeterx(headers, {}, url)
print(f"PerimeterX version: {px_version}")
This detection script gives you a quick read on whether PerimeterX is active, and if so, whether it’s using px2 or px3. You’ll often find telltale headers like x-px-authorization
or URL patterns tied to collector domains. Knowing the version you’re dealing with will help determine your approach later on.
Step 2: Understand Token Structure
Understanding how PerimeterX tokens are structured is key to bypassing the system. Let’s break it down:
PX3 tokens are formatted like this:3:hash:payload:appId:additional
def parse_px3_token(token):
parts = token.split(':')
if len(parts) < 5 or parts[0] != '3':
return None
return {
'version': parts[0],
'hash': parts[1],
'payload': parts[2], # Base64 encoded
'appId': parts[3],
'additional': parts[4]
}
# Example
token = "3:5b0de8f7a1d99c67add:MOXOASOAP5VNA==:1000:6xXzN9Q="
parsed = parse_px3_token(token)
print(parsed)
PX2 tokens, on the other hand, are simpler and usually look like this:2:base64_payload
import base64
import json
def parse_px2_token(token):
parts = token.split(':')
if len(parts) < 2 or parts[0] != '2':
return None
try:
# Decode base64 payload
payload = base64.b64decode(parts[1]).decode('utf-8')
# Parse JSON
json_data = json.loads(payload)
return {
'version': parts[0],
'decoded_payload': json_data
}
except Exception as e:
print(f"Error decoding token: {e}")
return None
# Example
token = "2:eyJ1Ijoi7hTyuIdsHHQ2cGxhdGZvcm0iOiJpT1MiLCJ0diI6IjMuMy4wIiwidGFnIjoidjMuMy4wIiwidXVpZCI6ImFiYzEyMzQ1Njc4OSIsImNzcyI6e30sInNjIjpbXSwiY3VzdG9tIjp7fSwiY3RzIjoxNjE5NjM0OTA2Njk3LCJ2aWQiOiJhMTIzNDU2NyIsInNpZCI6ImJkZWYyIn0="
parsed = parse_px2_token(token)
print(parsed)
Grasping the components of these tokens—especially the base64 payloads—is critical. It’ll allow you to decode them, troubleshoot when something goes wrong, and ultimately build your own.
Step 3: Generate Valid PX Tokens
Now comes the meat of the process: generating valid tokens that fool PerimeterX into thinking your requests are legitimate.
Method 1: Use an API-Based Generator
The most reliable route, especially in 2025, is to use a dedicated token-generation API like TakionAPI. These services simulate real devices and environments, producing valid tokens for use in apps like StockX.
import requests
def generate_px_token(app_id, device_info, px_version="px3"):
api_url = "https://px.takionapi.tech/generate"
payload = {
"appId": app_id,
"version": px_version,
"device": {
"os": device_info.get("os", "android"),
"osVersion": device_info.get("osVersion", "13"),
"deviceId": device_info.get("deviceId", ""),
"userAgent": device_info.get("userAgent", "")
}
}
headers = {
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
response = requests.post(api_url, json=payload, headers=headers)
if response.status_code == 200:
return response.json()
else:
print(f"Error generating token: {response.status_code}")
return None
# Example usage for StockX
device_info = {
"os": "android",
"osVersion": "13",
"deviceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"userAgent": "StockX/24.02.02 (Android 13; SM-G998B)"
}
token_data = generate_px_token("PX1000", device_info)
print(token_data)
Method 2: Build a Basic Token Generator
If you’re working on a smaller project or just experimenting, you can also roll your own simple PX3 token generator. Keep in mind, this approach won’t get you past heavily protected apps—but it’s a great way to learn how the system works under the hood.
import time
import json
import random
import hashlib
import base64
def generate_basic_px3_token(app_id, device_id):
# This is a simplified version - a real implementation would be more complex
# Create a basic payload
timestamp = int(time.time() * 1000)
payload_data = {
"t": timestamp,
"u": device_id,
"v": "3.3.0",
"os": "android",
"osv": "13",
"ts": {
"tst": timestamp - random.randint(100, 500),
"mst": random.randint(10, 50)
}
}
# Convert to JSON and encode to base64
payload_json = json.dumps(payload_data)
payload_b64 = base64.b64encode(payload_json.encode()).decode()
# Generate a fake hash - in a real scenario, this would use PerimeterX's algorithm
hash_value = hashlib.md5((payload_b64 + app_id).encode()).hexdigest()[:16]
# Additional data - normally contains signature information
additional = base64.b64encode(str(random.random()).encode()).decode()[:8] + "="
# Construct the token
token = f"3:{hash_value}:{payload_b64}:{app_id}:{additional}"
return token
# Example usage
app_id = "1000"
device_id = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
token = generate_basic_px3_token(app_id, device_id)
print(token)
The takeaway here is this: while custom solutions might work for less secure implementations, most real-world scenarios will require using or mimicking a legitimate mobile environment.
Step 4: Implement Tokens in Your Requests
Once you’ve got a valid token, you need to make sure it’s integrated properly in your requests. That typically means adding it to your headers—often under x-px-authorization
, though it can vary by app.
def make_px_request(url, px_token, cookies=None):
headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 13; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36",
"Accept-Language": "en-US,en;q=0.9",
"x-px-authorization": px_token
}
session = requests.Session()
if cookies:
for key, value in cookies.items():
session.cookies.set(key, value)
response = session.get(url, headers=headers)
if response.status_code == 200:
print("Request successful!")
return response
else:
print(f"Request failed with status code: {response.status_code}")
print(response.text[:200]) # Print first 200 chars for debugging
return None
# Example usage
url = "https://api.stockx.com/v2/products/search?query=jordan"
response = make_px_request(url, token)
if response:
data = response.json()
print(json.dumps(data, indent=2)[:500]) # Print first 500 chars
Keep your headers consistent with what the app expects. That includes the correct User-Agent string, language preferences, and of course, the token itself. If your request gets blocked, check your headers first—tiny mismatches can trigger PerimeterX defenses.
Step 5: Maintain Session Validity
Here’s something a lot of developers overlook: PX tokens aren’t meant to last. Many expire within 60 seconds, so unless you’re updating them regularly, you’ll find yourself blocked again before long.
To keep things running smoothly, I built a session class that handles token refresh and retry logic automatically.
class PerimeterXSession:
def __init__(self, app_id, device_info):
self.app_id = app_id
self.device_info = device_info
self.session = requests.Session()
self.px_token = None
self.token_generation_time = 0
self.token_expiry = 60 # Tokens typically expire after 60 seconds
# Set up session headers
self.session.headers.update({
"User-Agent": device_info.get("userAgent", "Mozilla/5.0 (Linux; Android 13; SM-G998B)"),
"Accept-Language": "en-US,en;q=0.9"
})
# Generate initial token
self._refresh_token()
def _refresh_token(self):
token_data = generate_px_token(self.app_id, self.device_info)
if token_data and "token" in token_data:
self.px_token = token_data["token"]
self.token_generation_time = time.time()
return True
return False
def _is_token_expired(self):
return (time.time() - self.token_generation_time) > self.token_expiry
def request(self, method, url, **kwargs):
# Check if token needs refreshing
if self._is_token_expired():
self._refresh_token()
# Add token to request headers
headers = kwargs.pop("headers", {})
headers["x-px-authorization"] = self.px_token
# Make the request
response = self.session.request(method, url, headers=headers, **kwargs)
# If we get a 403, try refreshing the token and retry
if response.status_code == 403:
if self._refresh_token():
headers["x-px-authorization"] = self.px_token
response = self.session.request(method, url, headers=headers, **kwargs)
return response
def get(self, url, **kwargs):
return self.request("GET", url, **kwargs)
def post(self, url, **kwargs):
return self.request("POST", url, **kwargs)
# Example usage
device_info = {
"os": "android",
"osVersion": "13",
"deviceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"userAgent": "StockX/24.02.02 (Android 13; SM-G998B)"
}
px_session = PerimeterXSession("PX1000", device_info)
response = px_session.get("https://api.stockx.com/v2/products/search?query=jordan")
print(f"Status code: {response.status_code}")
if response.status_code == 200:
data = response.json()
print(f"Found {len(data.get('products', []))} products")
This setup checks whether your token has expired, refreshes it when needed, and retries failed requests after getting a fresh one. It’s a massive time-saver and helps prevent unnecessary blocks.
Common Challenges and Solutions
Even with the right token, PerimeterX can still block you if other parts of your setup don’t look right. Here’s how to deal with the most common pitfalls:
1. Device Fingerprinting
PerimeterX looks closely at your device's fingerprint. To keep your session believable, you need consistent and realistic device info.
import uuid
def generate_consistent_device_info(app_name, seed=None):
# Generate deterministic device ID based on seed
if seed:
random.seed(seed)
# Popular Android devices in 2025
devices = [
{"model": "SM-S9300", "manufacturer": "Samsung", "os_version": "14"},
{"model": "Pixel 9", "manufacturer": "Google", "os_version": "15"},
{"model": "M3 Note", "manufacturer": "Xiaomi", "os_version": "14.5"}
]
selected_device = random.choice(devices)
# Generate a stable device ID based on seed
device_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"{app_name}:{seed}")) if seed else str(uuid.uuid4())
user_agents = {
"StockX": f"StockX/24.05.01 (Android {selected_device['os_version']}; {selected_device['model']})",
"TextNow": f"TextNow/24.5.1 (Android {selected_device['os_version']}; {selected_device['model']})",
"GrubHub": f"GrubHub/2025.5.1 (Android {selected_device['os_version']}; {selected_device['model']})"
}
return {
"os": "android",
"osVersion": selected_device["os_version"],
"deviceId": device_id,
"model": selected_device["model"],
"manufacturer": selected_device["manufacturer"],
"userAgent": user_agents.get(app_name, f"{app_name}/1.0 (Android {selected_device['os_version']}; {selected_device['model']})")
}
# Usage example - providing a seed ensures the same device info each time
device_info = generate_consistent_device_info("StockX", seed="your-stable-seed")
print(device_info)
Use a consistent seed when generating device info so your fingerprints remain stable across requests. Don’t skip this step—it plays a big role in passing fingerprint checks.
2. IP Rotation
Sticking with one IP is a sure way to get flagged. Rotate proxies often, and make sure they’re residential or mobile if possible.
def create_proxy_session(proxy_list):
# Create a session with proxy rotation
session = requests.Session()
# Select a random proxy
proxy = random.choice(proxy_list)
session.proxies = {
"http": proxy,
"https": proxy
}
return session
# Example
proxies = [
"http://user:pass@proxy1.example.com:8080",
"http://user:pass@proxy2.example.com:8080"
]
session = create_proxy_session(proxies)
High-quality proxies are essential for staying under the radar. Avoid datacenter IPs—they tend to get blacklisted quickly.
3. App Version Changes
Apps update frequently, and so do their anti-bot defenses. Keeping tabs on version changes can help you stay ahead of new challenges.
from bs4 import BeautifulSoup
import requests
def check_app_version(package_name):
url = f"https://play.google.com/store/apps/details?id={package_name}"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
soup = BeautifulSoup(response.text, 'html.parser')
# Look for version information in the Play Store page
version_element = soup.select_one('div[itemprop="softwareVersion"]')
if version_element:
return version_element.text.strip()
else:
# Alternative method if the first one fails
version_pattern = r'Current Version</div><span class="htlgb">([^<]+)<'
match = re.search(version_pattern, response.text)
if match:
return match.group(1)
return None
# Example usage
package_name = "com.stockx.stockx"
latest_version = check_app_version(package_name)
print(f"Latest version: {latest_version}")
If your token suddenly stops working, the app may have changed how it implements PerimeterX. Track app versions so you’re never caught off guard.
Final Thoughts
Getting around PerimeterX Mobile in 2025 isn’t easy—but it’s far from impossible. With the right tools, a structured approach, and a little patience, you can successfully automate even the most locked-down mobile apps.
Keep in mind that these techniques require upkeep. Monitor your success rates, test new devices and proxies, and tweak your tokens as needed. And always be mindful of the legal and ethical boundaries—you’re responsible for how you use this information.
This guide is designed to help developers understand and navigate PerimeterX protection in a responsible way. Use it to level up your automation workflows—and if you’ve hit other snags or found workarounds not mentioned here, feel free to share them.