Gamepad, WebXR & Device API Fingerprint Signals: The Vectors Few Teams Check

11 min read

Most fingerprinting write-ups stop at canvas, WebGL and audio. A quieter group of browser APIs also tells a site what class of hardware sits behind a session: navigator.getGamepads(), WebXR session support, WebUSB, WebHID and WebBluetooth. They carry little entropy on their own, but they are cheap to read and they have to agree with everything else a browser reports.

This guide is written for site owners, QA engineers and privacy-minded users. It shows what each device API exposes, how to read it in your own browser, and why a consistent profile reports the same hardware story everywhere. It also covers how a multi-account browser such as JustBrowser keeps that story coherent when several people share profiles.

What We're Covering

By the end of this, you'll know:

  1. Which niche device APIs create fingerprint surface (Gamepad, WebXR, WebUSB, WebHID, WebBluetooth)
  2. How each API exposes hardware class: desktop, mobile or headset-connected
  3. Why JavaScript overrides leave traces that a script can read
  4. How to test your own profiles for device API inconsistencies
  5. What native-engine handling looks like compared with extension-based workarounds

This builds on the battery, sensor and pointer API post. Same theme, different vectors. The sensor APIs there are well documented in fingerprinting literature. The ones below get far less attention.

Prerequisites

  • A multi-account browser with at least one profile configured
  • Basic JavaScript console familiarity
  • 20-30 minutes
  • Some background on why fingerprint consistency matters (the JA4 and TLS fingerprint guide covers fundamentals)

Step 1: The Gamepad API as a Fingerprint Vector

The Gamepad API lets web games detect controllers. The implementation details vary across platforms in ways that reveal device class:

// Check basic API availability
console.log('getGamepads:', 'getGamepads' in navigator);
console.log('Gamepad in window:', 'Gamepad' in window);

// Check array structure
const gamepads = navigator.getGamepads();
console.log('Array type:', Object.prototype.toString.call(gamepads));
console.log('Array length:', gamepads ? gamepads.length : 'null');
console.log('Contents:', gamepads);

Desktop Chrome (Windows and macOS): returns a list with 4 slots, all null if no controller is connected.

Android Chrome: the API exists but behavior varies by device and Chrome version. Some return null directly, some throw, some return an empty array-like object.

iOS Safari: the Gamepad API is supported, with different capabilities than desktop browsers.

A site can check whether getGamepads() returns the structure expected for the platform a browser claims to be. A session that reports an Android user agent but returns a clean 4-slot desktop list is reporting desktop behavior. For teams tracking analytics across many browser profiles, JustAnalytics can help correlate session data with the environment each session reports.

The practical lesson: a profile should only present a device class it can actually render. Don't give a profile a phone's user agent and expect desktop Chromium's APIs to agree with it. For how sites combine signals like these, see how AI-driven bot detection works.

Step 2: WebXR Session Support Fingerprinting

WebXR is the successor to WebVR and handles both virtual and augmented reality. Even if you have never touched a headset, the API exists in your browser, and its capabilities reveal information:

if ('xr' in navigator) {
  console.log('WebXR available');

  // Check session support
  navigator.xr.isSessionSupported('inline').then(supported => {
    console.log('inline session:', supported);
  });

  navigator.xr.isSessionSupported('immersive-vr').then(supported => {
    console.log('immersive-vr session:', supported);
  });

  navigator.xr.isSessionSupported('immersive-ar').then(supported => {
    console.log('immersive-ar session:', supported);
  });
} else {
  console.log('WebXR not available');
}

Standard desktop Chrome without VR: inline: true, immersive-vr: false, immersive-ar: false

Desktop with SteamVR installed: inline: true, immersive-vr: true, immersive-ar: false (usually)

Desktop with Quest Link: a different capability set depending on the Oculus software state

Android Chrome with ARCore: inline: true, immersive-vr: false, immersive-ar: true

iOS Safari: availability varies by iOS version and often requires a user gesture to check

The combination of these three booleans forms a device-class signal. A laptop reporting immersive-vr: true suggests VR software is installed, which may be perfectly true or may disagree with other parameters. This is not high-entropy fingerprinting like canvas. It is device-class classification, and it is trivial for a script to run.

Step 3: WebUSB, WebHID, and WebBluetooth Availability

These three APIs let websites reach USB devices, HID peripherals (keyboards, game controllers) and Bluetooth devices. Actual access requires user permission, but their presence in the browser already tells a script something:

// API availability checks
console.log('USB in navigator:', 'usb' in navigator);
console.log('HID in navigator:', 'hid' in navigator);
console.log('Bluetooth in navigator:', 'bluetooth' in navigator);

// Deeper checks
if ('usb' in navigator) {
  console.log('USB getDevices:', typeof navigator.usb.getDevices);
  console.log('USB requestDevice:', typeof navigator.usb.requestDevice);
}

if ('hid' in navigator) {
  console.log('HID getDevices:', typeof navigator.hid.getDevices);
  console.log('HID requestDevice:', typeof navigator.hid.requestDevice);
}

Desktop Chrome: all three APIs are typically available Android Chrome: WebUSB limited, WebHID unavailable, WebBluetooth available with restrictions iOS Safari: none of these APIs exist Firefox: generally none of these APIs

These are binary checks that take a few lines of JavaScript, which is why they are a common first-pass consistency test: a browser that says one thing in its user agent and another in its API surface has an internal contradiction.

Extension-based tools cannot cleanly remove these APIs. They can override navigator.usb to undefined, but that breaks sites that legitimately check for WebUSB, and the override leaves traces in prototype chains. Native Chromium builds decide API availability in the engine itself, so the answer is the same everywhere the API is read.

Step 4: Ambient Device Hints and Hardware Class

Beyond the explicit device APIs, browsers expose hints about hardware class through other interfaces:

// Media Devices — cameras and microphones
if ('mediaDevices' in navigator) {
  navigator.mediaDevices.enumerateDevices().then(devices => {
    console.log('Media devices:', devices.length);
    devices.forEach(d => console.log(d.kind, d.label || '[no label]'));
  });
}

// Device Memory API
console.log('Device memory:', navigator.deviceMemory, 'GB');

// Hardware Concurrency (CPU cores)
console.log('Hardware concurrency:', navigator.hardwareConcurrency);

// Connection type
if ('connection' in navigator) {
  console.log('Connection type:', navigator.connection.effectiveType);
  console.log('Downlink:', navigator.connection.downlink, 'Mbps');
}

Device memory and hardware concurrency are covered in the audio, font and hardware fingerprinting post. Combined with the device APIs, a phone-class identity would be expected to report:

  • A modest deviceMemory and hardwareConcurrency for its class
  • No WebUSB, WebHID or WebBluetooth (on iOS)
  • Specific Gamepad API behavior
  • No immersive-vr WebXR support
  • One or two media devices (front and back camera, microphone)

If these signals contradict one another, a consistency check will notice. What matters is the cross-product of all device-class signals telling one coherent story, not any single value.

Step 5: Testing Your Profiles

Here is a diagnostic script that reads the device API surface:

// Device API Fingerprint Diagnostic
const results = {
  gamepad: {
    apiExists: 'getGamepads' in navigator,
    result: null
  },
  webxr: {
    apiExists: 'xr' in navigator,
    inline: null,
    immersiveVr: null,
    immersiveAr: null
  },
  deviceApis: {
    webusb: 'usb' in navigator,
    webhid: 'hid' in navigator,
    bluetooth: 'bluetooth' in navigator
  },
  hardware: {
    memory: navigator.deviceMemory,
    cores: navigator.hardwareConcurrency
  }
};

// Gamepad check
try {
  const gp = navigator.getGamepads();
  results.gamepad.result = {
    type: Object.prototype.toString.call(gp),
    length: gp ? gp.length : null,
    isNull: gp === null
  };
} catch (e) {
  results.gamepad.result = { error: e.message };
}

// WebXR check
if (results.webxr.apiExists) {
  Promise.all([
    navigator.xr.isSessionSupported('inline'),
    navigator.xr.isSessionSupported('immersive-vr'),
    navigator.xr.isSessionSupported('immersive-ar')
  ]).then(([inline, vr, ar]) => {
    results.webxr.inline = inline;
    results.webxr.immersiveVr = vr;
    results.webxr.immersiveAr = ar;
    console.log('Device API Fingerprint:', JSON.stringify(results, null, 2));
  });
} else {
  console.log('Device API Fingerprint:', JSON.stringify(results, null, 2));
}

Run this in a profile's console, then run it on a real device of the type that profile reports. Compare the outputs.

For a desktop profile, look for:

  • Missing APIs that desktop Chrome should have
  • Hardware values that do not match the platform the profile reports
  • WebXR capabilities that suggest VR software the profile is not meant to have

For QA across device classes, test on real phones or a device farm and treat any desktop profile as a desktop.

Step 6: Why JavaScript Overrides Fall Short

A common idea is to override navigator.getGamepads in script. Three problems make that fragile for anyone who needs trustworthy results:

Property descriptor traces. Overriding a native property changes its descriptor. Object.getOwnPropertyDescriptor(navigator, 'getGamepads') returns different values for native and overridden properties.

Prototype modification. Overriding APIs often means changing prototypes, and scripts can hash prototype chains to notice. The lie-detection section of CreepJS is built around this idea.

Cross-context consistency. A main-thread override does not apply to Web Workers or Service Workers. A script can call the same API from several contexts and compare answers.

Patching Chromium's C++ at build time is a different approach: the API behaves as the binary does, so there is no script layer to disagree with itself. JustBrowser's native Chromium engine works this way: on a desktop profile, the device API surface stays consistent with the identity the profile uses. The result for teams is simple. Each profile is its own browser, and its answers agree across main thread, workers and frames.

Common Errors and Fixes

Error: Gamepad API returns a desktop structure on a mobile-claiming profile

Cause: The tool let the profile claim a mobile device while the engine kept answering like desktop Chromium.

Fix: Do not claim a device class your browser cannot present consistently. If you need mobile renders for ad verification testing, use a real device or a device farm.

Error: WebXR returns immersive-vr: true unexpectedly

Cause: VR software (SteamVR, Oculus and similar) is installed on the host and the profile reports it.

Fix: Uninstall VR runtimes on the host if you do not need them, or accept that the profile reports a VR-capable desktop and keep other signals consistent with that.

Error: WebHID or WebUSB present on a mobile-claiming profile

Cause: Extension-based tools cannot remove these APIs cleanly.

Fix: Use a native-engine tool, or only generate identities your engine can render consistently.

Error: Each device signal looks right on its own, but they don't agree

Cause: A cross-signal mismatch. Device APIs say one thing, the user agent another, hardware values a third.

Fix: Align every signal with the platform the profile reports. The combination matters more than individual values.

Next Steps

  1. Run the diagnostic on every profile your team uses for client work or automation. Record what each reports for Gamepad, WebXR, WebUSB/WebHID/WebBluetooth and hardware values.

  2. Compare with real devices. Run the same diagnostic on actual hardware of the type you are testing for. The delta shows where a profile and a device disagree.

  3. Use public test pages. CreepJS examines some device APIs and FingerprintJS examines others. They are useful for understanding what your own site could learn about a visitor. For click-pattern monitoring on ad campaigns, ClickzProtect looks at traffic quality.

  4. Re-test after browser updates. Engines and web platform APIs change. JustBrowser's engine is patched on every upstream Chromium release, so a quarterly re-check of your reference profiles is enough.

The niche device APIs get little marketing attention, because canvas and WebGL dominate the fingerprinting conversation. They still matter for anyone building or testing a site: they show how much a browser tells you about hardware before a user clicks anything, and why a profile is only as coherent as its least consistent signal.

Frequently Asked Questions

Does the Gamepad API work without a physical controller connected?

The API exists regardless of hardware. navigator.getGamepads() returns an array of four slots, all null if no controllers are connected. But the API's presence, its timing characteristics, and how the browser handles gamepad events differ between desktop and mobile. A script can check whether getGamepads returns the expected array structure and whether the gamepadconnected event fires with realistic timing.

Can WebXR fingerprinting reveal VR headset models?

Partly. navigator.xr.isSessionSupported() returns different values based on installed XR runtimes. A machine with SteamVR returns true for immersive-vr mode, and a Quest Link setup returns different capabilities. Standard laptops without VR software return false for immersive modes but true for inline sessions. The combination of supported session modes (inline, immersive-vr, immersive-ar) forms a device-class signal.

Why are WebUSB and WebHID fingerprinting signals?

These APIs require user permission to access specific devices, but their mere existence in the window object reveals information. Desktop Chrome exposes WebUSB, WebHID and WebBluetooth. Mobile Chrome has limited or no support. A profile that reports Android but where 'usb' in navigator or 'hid' in navigator returns true contains an internal contradiction.

How do multi-account browsers handle niche device APIs?

Extension-based tools typically ignore them and focus on high-entropy vectors such as canvas and WebGL. Native Chromium builds decide API availability in the engine itself. JustBrowser's engine reports the device API surface as desktop Chromium does, so on a desktop profile Gamepad, WebXR, WebUSB and WebHID tell the same story as the rest of the profile.


Try JustBrowser

A multi-account browser for teams, built on a custom Chromium engine with native C++ integration, not an extension. Each profile is its own browser with its own cookies, storage, proxy and 40+ identity parameters, and profiles can be shared with teammates. Local REST API and CDP for Playwright, Puppeteer, Selenium. $9.99/month or $99.99/year. 7-day free trial, card required — cancel any time in the seven days and you are not charged. Unlimited profiles.

Get started → · How it differs from Multilogin / GoLogin / AdsPower

Tags
  • gamepad-api-fingerprinting
  • webxr-fingerprinting
  • webhid-webusb-detection
  • device-api-leaks
  • multi-account-browser
  • buildinpublic
  • saasstudio
  • aiworkforce
  • buildwithclaude

Ready to manage multiple accounts?

Seven days free, then $9.99/month — one plan, everything included.

We'd like to use Google Analytics, a Google service, to understand how our website is used. It sets two cookies in your browser and runs only if you click Accept. You can change your choice at any time with Cookie settings. Cookie Policy

Sign-in cookies and the cookie that remembers this choice are always on; the website needs them to work.

Google Analytics, a Google service, helps us understand how our website is used. It sets two cookies, _ga and _ga_TVZHQ99TZW. It is now onoff in this browser. If your browser sends a Global Privacy Control or Do Not Track signal, it stays off. Cookie Policy