Skip to main content
A banned user does the obvious thing to come back: clear cookies, open an incognito window, or register a brand new account. Each of those resets the VisitorID and looks like a first-time visitor to a cookie-based ban. The DeviceID does not move with any of them. It is derived server-side from dozens of stable device characteristics, so the same browser produces the same DeviceID after a cookie wipe, in incognito, and across an IP rotation. Ban the device, and the return shows up no matter how clean the cookie looks.

Key the ban on the DeviceID

Record the DeviceID (and the local IP) on a device-level banlist when you ban someone. It survives the cookie clear that a cookie-keyed ban does not.

Check it before the cookie matters

On every visit, compare the incoming DeviceID against your banlist first. A banned DeviceID under a new VisitorID or a new account is the return you are looking for.
ShieldLabs gives you the durable identifier and the Risk Score; your own code owns the verdict. The play here is one banlist lookup at the start of a session, keyed on something the returning user cannot easily change.
The DeviceID is browser-bound. A different browser on the same machine, or a wiped or materially changed device, can produce a new DeviceID, so treat your device-level counts as estimates and pair the DeviceID with the local IP. The honest limits are spelled out below.
When you ban a user, write down what travels with them. The cookie and the account both reset on demand; the DeviceID and the local IP are what a returning user has to keep using.
When you issue a ban
// At ban time you already have the offender's last scored session.
// Persist the durable identifiers, not just the account id.
async function banUser(accountId, shieldRequestId, reason) {
  const shield = await waitForScore(shieldRequestId, 2000);

  await markAccountBanned(accountId, reason);

  // Record the device-level keys so a cleared cookie or a fresh account
  // does not buy a clean slate. A blocked / all-zero DeviceID is not a key.
  const ZERO_DEVICE = '00000000-0000-0000-0000-000000000000';
  if (shield?.DeviceID && shield.DeviceID !== ZERO_DEVICE) {
    await banlist.addDevice(shield.DeviceID, { accountId, reason });
  }
  if (shield?.IP) {
    await banlist.addLocalIp(shield.IP, { accountId, reason });
  }
}
waitForScore is the shared webhook-cache read (poll the cache, then fall back to a History API read by request_id) that the Cookbook defines once, so this recipe only carries the banlist logic that is unique to ban evasion.

Check the banlist on every visit

Run the lookup at the start of the session, before you trust the cookie or even know which account this is. A banned DeviceID arriving under a fresh VisitorID is the tell that someone cleared storage to get back in.
Gate the session on the device banlist
app.post('/api/session-open', async (req, res) => {
  const { shieldRequestId } = req.body;

  const shield   = await waitForScore(shieldRequestId, 2000);
  const deviceId = shield?.DeviceID;
  const localIp  = shield?.IP;

  // A blocked or JavaScript-disabled session returns an all-zero DeviceID,
  // because no stable device characteristics were collected. That is
  // "device unknown", not a free pass: never auto-ban it, and never let it
  // slip a known banned visitor through. Route it to manual review and lean
  // on the local IP and your own context.
  const ZERO_DEVICE = '00000000-0000-0000-0000-000000000000';
  if (!deviceId || deviceId === ZERO_DEVICE) {
    return res.json({ action: 'review', reason: 'device_unknown' });
  }

  // The core check: is this device on the banlist, whatever the cookie says?
  if (await banlist.hasDevice(deviceId)) {
    return res.json({ action: 'block', reason: 'banned_device_returned' });
  }

  // Defense in depth: same local IP as a banned session, on a new device.
  // Weaker on its own (a shared router, an office NAT), so review, do not block.
  if (localIp && (await banlist.hasLocalIp(localIp))) {
    return res.json({ action: 'review', reason: 'banned_local_ip' });
  }

  return res.json({ action: 'allow' });
});
A returning banned DeviceID is a strong signal, the local IP is a soft one. Block on the device match; only review on the local IP match, since a banned user and an innocent one can share an office router or a home NAT. Keep the verdict in your code where you can log it and tune it.
Description is a human-readable label for visibility and logging. Branch on the Score and each entry’s Value, not on the label text, which can change.
The VisitorID lives in browser storage, so clearing cookies or opening an incognito window resets it, exactly the move a returning banned user makes. The DeviceID is computed on the server from stable browser and device characteristics rather than read from a cookie. Because it is derived and not stored, the same browser hands back the same DeviceID after the storage is gone. That durability across cookie clears, incognito, and IP rotation is what makes it a banlist key, and the identification reference covers how the two identifiers differ.

Where the all-zero DeviceID comes from

A session that blocks the fingerprint or runs with JavaScript disabled cannot produce stable device characteristics, so the server has nothing to derive a real DeviceID from. It returns the all-zero DeviceID (00000000-0000-0000-0000-000000000000) and scores it 90, in the High band. That high score reflects an evasive or non-cooperating client, not a confirmed ban.
Never auto-ban the all-zero DeviceID. Every blocked or JS-disabled visitor shares it, so banning it would lock out an entire class of clients at once and collapse many distinct visitors onto one key. Send the all-zero case to manual review, weigh it with the local IP and your own context, and let a human decide.

Spot the evasion pattern in the dashboard

The per-visit banlist stops a known device at the door. The dashboard Abuse Patterns show you the evasion shape over time, grading each flagged entity Suspicious or Dangerous as the count crosses a threshold in a rolling window. An entity below the first threshold is simply the unflagged baseline, never recorded as a grade.
One account whose VisitorID or DeviceID keeps changing, a hint of cleared storage, anti-detect tooling, or an environment cycled between visits to look new each time.
One device tied to many accounts, the shape of a banned user who keeps registering fresh accounts from the same machine to get back in.
Multiple accounts sharing one local IP across different devices and public IPs, a sign of coordinated returns or a ban dodged from behind shared infrastructure.
You read these on the dashboard Patterns tab, where each entity shows its grade and the identifiers behind it. Levels never downgrade: once an entity is flagged Dangerous, new clean activity does not clear it. Export the Dangerous DeviceIDs and local IPs (CSV or JSON) and feed them straight into your banlist as a watchlist.

Reconstruct a device’s history programmatically

You can also check what a device has done from the History API. Read by device_id to see every session and account that machine has touched, newest first.
Read one device's history
curl "https://api.shieldlabs.ai/{domain}:{secret}/history/device_id/5eb7fd5c-1c5e-4a9f-9b21-7d2e8c0a1234?limit=100"
Confirm a banned device's return
async function deviceHistory(deviceId) {
  const rows = await shieldHistory('device_id', deviceId, 100);
  const accounts = new Set(rows.map((r) => r.UserHID).filter(Boolean));
  return { sessions: rows.length, accounts: accounts.size };
}

// A banned device coming back under brand new accounts confirms the evasion.
const { accounts } = await deviceHistory(deviceId);
if (await banlist.hasDevice(deviceId)) {
  flagForReview(deviceId, { accounts, note: 'banned device active under new accounts' });
}
The History API bills 1 request per returned row (an empty result still bills 1), whereas the webhook delivery is free. Lean on the per-visit banlist lookup and the pre-computed pattern export for routine enforcement, and reserve live device_id reads for the cases you are actively confirming.

Putting it together

1

Identify the session

Load the snippet and run an identification at the start of the visit so the DeviceID and local IP are available before you trust the cookie.
2

Record device keys at ban time

When you ban a user, persist their DeviceID and local IP on a device-level banlist, not just the account id, as covered in the webhooks setup for getting those fields.
3

Check the banlist before the cookie

On every visit, look up the incoming DeviceID first. Block a banned device; review a banned local IP on a new device.
4

Route the all-zero DeviceID to review

A blocked or JS-disabled session returns the all-zero DeviceID and scores 90. Never auto-ban it. Send it to manual review with the IP and account context.
5

Watch the patterns and tune

Pull “Changing IDs on One Account”, “Many Accounts on One Device”, and “Shared Local IP Across Multiple Accounts” from the dashboard, feed the Dangerous entities into your banlist, and start in logging-only mode before you turn on hard blocks.
A guide, not a rule. The local IP is a soft key on purpose, since legitimate visitors share networks.
ConditionSuggested action
Incoming DeviceID on your banlistBlock, the banned device has returned
New device, but local IP matches a banned sessionReview, could be a shared network
All-zero DeviceID (blocked or JS-disabled, score 90)Manual review, never auto-ban
”Changing IDs on One Account” or “Many Accounts on One Device” (Dangerous)Add the entity to your banlist, then review
Clean device, no banlist matchAllow

Next: Stop Multi-Accounting at Signup

The companion pattern for the registration step: catch the fresh accounts a banned user opens before they get created.