Skip to main content
Account sharing has a recognizable shape on the wire: one account, many devices, sometimes many countries in a short window. ShieldLabs gives you four layers to see it, and your own code decides the policy (a paid seat is fine, a credential resold to fifty people is not).

What is account sharing?

Account sharing is when one set of login credentials is used across more people or devices than a plan allows — a password handed to friends, a single seat split across a team, or a subscription resold to many strangers. It shows up as one account appearing on more distinct devices and locations than a single user could plausibly produce.

How ShieldLabs surfaces it

ShieldLabs resolves each authenticated session to a set of identifiers and grades how far an account has spread. Four layers answer four different questions:
LayerWhat it answersWhere you read itLatency
Identification”Is this the same device, and is it a new device or country for this account?”The durable device_id on the webhook / History APIAbout a second
Anonymity detection”Is this session masked or anonymous right now?”The signals array on the webhook / History APIAbout a second
Risk Score”How risky is the visit overall, as one 0-100 number?”risk_score on the webhook / History APIAbout a second
Patterns”Has this account spread across many devices or countries over time?”Dashboard Patterns + exportBackground (~10 min)
The anchor for the rest is the durable DeviceID — derived server-side, so a sharer cannot reset it by clearing cookies, opening an incognito window, or switching networks. Counting an account’s devices by cookie or IP undercounts badly, because each of those reads as a fresh device; the DeviceID holds steady, so a credential reused on the same machine still resolves to one device instead of inflating the count.
Account sharing is a policy question, not a fraud verdict. A family plan, a shared team login, and a resold credential can all look like “many devices on one account.” ShieldLabs tells you the spread; your terms of service decide what is allowed.

Prevent account sharing

The rule your code applies: for each account (UserHID), count the distinct devices and session countries you have seen, and when that count crosses your plan’s limit, step up verification, notify the account owner, or restrict the extra sessions instead of letting one credential run everywhere at once. Weigh a masked session more heavily — when a sharer hides behind a VPN to look local, the real network IP (local_ip) still exposes the network behind a faked public_ip.country, and detection_flags.ip_mismatch is set to true. ShieldLabs surfaces the spread and the Risk Score (0–100); your code owns the limit and the verdict. The steps below wire it up.

Build it

1

Create a ShieldLabs account and get your keys

Sign up for free and get 5,000 identifications, or log in if you already have an account. Register the domain you want to identify visitors on, then open the Keys page. Use the Public Key to initialize the snippet in the browser, and keep your server-side credentials on your backend: the Private API Key authenticates the History API, and each webhook endpoint has its own whsec_… signing secret. See Keys.
2

Identify authenticated sessions

Add the snippet to your app and call checkAuthenticatedUser with the account’s hashed id (UserHID) on login and on sensitive actions. Pass a hash, never a raw email or user id.
app.html
<script type="module">
  const mod = await import(
    'https://cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_PUBLIC_KEY'
  );
  // Pass the hashed account id, never a raw email or user id.
  mod.checkAuthenticatedUser('8a9f-hashed-account-id', (ip, requestID) => {
    document.getElementById('shield-request-id').value = requestID;
  });
</script>
3

Check the device on every session

Read the scored result for that RequestID from your webhook cache (the shared waitForScore helper), or fall back to the History API, then compare the DeviceID against the account’s known devices.
api/session-check.js
app.post('/api/session-check', async (req, res) => {
  const { accountId, shieldRequestId } = req.body;

  // Pull the ShieldLabs result for this session (snake_case, as the
  // webhook and account History API deliver it).
  const shield = await waitForScore(shieldRequestId, 2000);
  const deviceId = shield?.device_id;
  const country  = shield?.public_ip?.country;

  // A blocked or JS-disabled session can return an all-zero DeviceID.
  // That is "device unknown", not a new device: weigh it with IP + account
  // context instead of treating it as one more device or auto-allowing.
  const ZERO_DEVICE = '00000000-0000-0000-0000-000000000000';
  if (!deviceId || deviceId === ZERO_DEVICE) {
    return res.json({ action: 'review', reason: 'device_unknown' });
  }

  // Compare against what you already know about this account.
  const known = await knownDevicesFor(accountId);   // your own store

  if (!known.devices.has(deviceId)) {
    // A device this account has never used. Your call: re-auth, notify, or log.
    if (known.devices.size >= YOUR_DEVICE_LIMIT) {
      return res.json({ action: 'reauth_required', reason: 'new_device_over_limit' });
    }
    await rememberDevice(accountId, deviceId, country);
    return res.json({ action: 'notify_new_device' });
  }

  return res.json({ action: 'allow' });
});
Read the real country behind a VPN. The webhook carries two geo readings. public_ip.country comes from the public IP, which a VPN exit can put anywhere the sharer wants. local_ip.country is the visitor’s real network IP, which can reveal the network they are actually on. When the two disagree, detection_flags.ip_mismatch is set to true. So a credential whose public_ip.country roams while local_ip.country stays fixed is one masked location, not real spread — read the two together before you count an account’s countries.
One person using two browsers (Chrome then Safari) shows up as two devices, so “many devices” can include a single person’s own browsers, a nuance the identifiers reference explains. Weigh it with the country spread, the IP, and your own context before you treat it as sharing.
4

See the spread over time

Identification catches a new device right now. The harder signal is an account that quietly appears on a dozen devices, or from several countries within an hour. Patterns grade each account Suspicious, then Dangerous, as the linked count crosses thresholds in a rolling window (an account below the first threshold is the unflagged baseline). Levels never downgrade: once flagged Dangerous, new clean activity does not clear it.
One account used from many different devices. The core account-sharing and account-resale shape. The grouping identity is the UserHID; the spread is counted in distinct DeviceIDs over a rolling window (default 30 days).
The same account active from several countries in a short window (24 hours). Catches a credential shared across regions, or one used behind rotating VPN exits. A real traveler can trip this, so read it with the device spread.
An existing account suddenly appears from a device and a country it has never used together. A strong account-takeover or hand-off signal, distinct from steady sharing.
Read these on the dashboard Patterns tab, or reconstruct the same counts live from the History API:
Count devices and countries per account
async function accountSpread(userHid) {
  const rows = await shieldHistory('user_hid', userHid, 100);
  const devices   = new Set(rows.map((r) => r.device_id).filter(Boolean));
  const countries = new Set(rows.map((r) => r.country).filter(Boolean));

  // The device count is the durable signal. The country count is read from
  // the public IP, which a VPN can fake, so treat it as the softer signal.
  // For a live verdict, the webhook's detection_flags.ip_mismatch boolean
  // tells you a session's public country is masked, no label parsing needed.
  return { devices: devices.size, countries: countries.size };
}

const { devices, countries } = await accountSpread('8a9f-hashed-account-id');
if (devices >= YOUR_DEVICE_LIMIT || countries >= YOUR_COUNTRY_LIMIT) {
  flagForReview(accountId, { devices, countries });
}
Account History reads on account.shieldlabs.ai and webhook delivery are free; the Management History path bills 1 request per returned row (an empty result still bills 1). For routine enforcement, use the dashboard pattern export as your watchlist and reserve billed Management reads for accounts you are actively investigating.
5

Tune to your product

A streaming service tolerates more devices than a single-seat B2B tool. Start in logging-only mode, watch how your real accounts distribute, then set the device and country limits that match your terms.

Test it

Confirm the durable DeviceID holds before you wire policy to it. Log into one test account, then revisit the same machine in an incognito window, after clearing cookies, and from a second browser: the cookie_id and visitor_id change each time, but the device_id stays the same, so one machine does not look like three devices. Then log the same account in from a second physical device and watch a genuinely new device_id (and public_ip.country) appear — that is the real “new device” your check should act on. A guide, not a rule. The right device and country limits depend entirely on your product.
SignalSuggested action
Known device, known countryAllow
New device, within your device limitNotify the account owner, remember the device
New device, over your device limitRequire re-authentication on the new device
”Multiple Countries on One Account” (Suspicious or Dangerous)Step up verification, review against your sharing policy
”New Device and New Country” (Dangerous)Treat as possible takeover: force re-auth with the Login and 2FA step-up pattern
A sudden device-and-country jump on an existing account can be sharing, but it can also be account takeover or the tail of a credential-stuffing run. The same per-session DeviceID and webhook payload feed all three, so once you have this wired you can branch on intent.

Next: Acting on the Risk Score

The full decision playbook, including how to combine identity spread with the per-session Risk Score and its signals.