Learn how to detect and prevent account sharing by seeing how many devices and countries each account is used from, and enforce your sharing policy.
Account sharing has a recognizable shape on the wire: one account, many devices, sometimes many countries in a short window. ShieldLabs gives you five layers to see it, including the Account sharing High-Risk Event, and you choose the action for each case (a paid seat is fine, a credential resold to fifty people is not).
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.
ShieldLabs ties each identification of a signed-in user to the account and to the device behind it, and shows how far the account has spread. Five layers answer five different questions:
Layer
What it answers
Where you read it
Latency
Account
”How many devices and countries has this account used?”
The History API by user_hid: the account’s devices, countries and worst band
On demand
Identification
”Is this the same device, and is it a new device or country for this account?”
The anchor for the rest is the durable Device ID, derived on the server, 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 Device ID holds steady, so a credential reused on the same device still resolves to one device instead of inflating the count.
Account sharing is a policy question. A family plan, a shared team login and a resold credential can all show as one account on many devices. ShieldLabs detects the sharing and shows the account’s devices; you choose the action for each case under your terms of service.
When the Account sharing High-Risk Event arrives for a user through the API or webhooks, or when you review it in the analytics dashboard, act on the account: mark it in your own system, so its next sessions step up verification, notify the account owner, or restrict the extra sessions instead of letting one credential run everywhere at once. For a live check against your plan’s seat limit, count the distinct devices and countries per account, as the steps below show. Weigh a masked session more heavily: when a sharer uses a VPN to look local, local_ip, the address the browser itself reports, can show a different country from the public IP. ShieldLabs detects the spread and scores each identification with the Risk Score (0-100); you choose the action for each case in your backend. The steps below wire it up.
Start Free with 5,000 identifications, one time, no credit card, or log in. In the analytics dashboard, add the domain you want to protect under Integration > Domains, then open Integration > API keys and copy its keys with the copy button next to each. The Public Key loads the snippet in the browser. Keep the server credentials on your backend: the Private API Key reads the History API, and each webhook endpoint has its own whsec_… signing secret. See API keys and Integration.
2
Identify authenticated sessions
Add the snippet to your app and pass the account’s hashed User HID with checkAuthenticatedUser on every signed-in page. Account sharing, Impossible travel and every account-level read below are built on it. When a session starts (right after login), call forceCheckAuthenticatedUser instead, so the new session always gets its own identification: a plain check is skipped when the same user was checked in this visit within the last five minutes. 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' ); // Right after login: a fresh identification for the new session. // On other signed-in pages, checkAuthenticatedUser with the same hashed id. mod.forceCheckAuthenticatedUser('8a9f-hashed-account-id', { onInitialized: (result) => { fetch('/api/session-check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ shieldlabsRequestId: result.requestID ?? '' }), }); }, });</script>
3
Check the device on every session
Read the scored result for that request ID with the shared waitForScore helper (your webhook cache, falling back to the History API), then compare the Device ID against the account’s known devices.
api/session-check.js
app.post('/api/session-check', async (req, res) => { const { shieldlabsRequestId } = req.body; const userHid = req.user.hashedId; // the hashed id you pass to the snippet // Pull the ShieldLabs result for this session: the webhook `data` object // (the shared helper maps a History fallback to the same field names). const risk = await waitForScore(shieldlabsRequestId, 2000); if (!risk || risk.user_hid !== userHid || risk.risk_score > 100) { // No identification, someone else's, or the 999 rate-limit marker. return res.json({ action: 'review', reason: 'unverified_session' }); } const deviceId = risk.device_id; const country = risk.public_ip?.country; // An all-zero Device ID means no usable device signals reached ShieldLabs: // "device unknown", not a new device. Route it to review. if (deviceId === NIL_DEVICE) { return res.json({ action: 'review', reason: 'device_unknown' }); } // Compare against what you already know about this account. const known = await knownDevicesFor(userHid); // your own store if (!known.devices.has(deviceId)) { // A device this account has never used. Choose the action: 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(userHid, deviceId, country); return res.json({ action: 'notify_new_device' }); } return res.json({ action: 'allow' });});
Read the country with care behind a VPN.public_ip.country comes from the public IP, which a VPN exit can put anywhere. local_ip.country belongs to the Local IP, the address the browser itself reports, which can differ from the public IP behind a VPN or proxy; local_ip.ip is empty when the Local IP was not captured. When the two addresses differ, detection_flags.ip_mismatch is true. The flag is informational, adds nothing to the Risk Score and can be ordinary on mobile networks, so compare local_ip.country with public_ip.country rather than branching on the flag alone. A credential whose public_ip.country roams while local_ip.country stays fixed is more likely one masked location than real spread, so 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 spreads across many devices, or appears in places one person could not reach in the time between them. ShieldLabs detects these on your users as High-Risk Events, each at Medium or High confidence, and they are available in the analytics dashboard, the API and webhooks. Events are a separate axis from the Risk Score and are keyed on the User HID you pass with checkAuthenticatedUser.
An account with an Account sharing event in the analytics dashboard, with the devices linked to it.
Account sharing
One account used from several distinct devices. By default it fires from 4 devices on one account, and the threshold is configurable. The core account-sharing and account-resale shape, keyed on the User HID.
Impossible travel
The same account active in locations it could not reach in the time between them. Catches a credential shared across regions; read it with the device spread.
Account takeover
An existing account appearing in a new environment that points to someone else using it. A takeover or hand-off signal, distinct from steady sharing.
To reconstruct the device and country counts live, read the account’s identifications from the History API with the shared accountView helper:
Count devices and countries per account
const account = await accountView(userHid);// The device count is the durable signal. The country count is read from// the public IP, which a VPN can put anywhere, so treat it as the softer signal.// For a single identification, detection_flags.ip_mismatch marks a public IP// that differs from local_ip. It is informational: weigh it, do not gate on it.if (account.devices.size >= YOUR_DEVICE_LIMIT || account.countries.size >= YOUR_COUNTRY_LIMIT) { flagForReview(userHid, { devices: account.devices.size, countries: account.countries.size, });}
History reads on account.shieldlabs.ai and webhook delivery are free. High-Risk Events are available in the analytics dashboard, the API and webhooks. When an Account sharing event arrives for a user through the API or webhooks, or when you review it in the analytics dashboard, add the User HID, with the devices and local IPs linked to it, to a watchlist in your datastore; at the session check, check the incoming user_hid and device_id against it. The History API returns every device an account has used, by user_hid.
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 in your own session check to match your terms.
Confirm the Device ID holds before you wire policy to it. Log in to one test account, then revisit on the same machine and browser in an incognito window and after clearing cookies: the cookie_id and visitor_id change each time, but the device_id stays the same, so one browser does not look like three devices. Then log the same account in from a second browser or a second device: a new device_id appears. That is the new device your check counts, and the reason your device limit should allow for one person’s own browsers.
A guide, not a rule. The right device and country limits depend entirely on your product.
Signal
Suggested action
Known device, known country
Allow
New device, within your device limit
Notify the account owner, remember the device
New device, over your device limit
Require re-authentication on the new device
No identification for the session, or an all-zero Device ID
Review before you count the device
Account with an Account sharing event, Medium confidence
Notify the account owner, review against your sharing policy
Account with an Account sharing event, High confidence
Step up verification and restrict the extra sessions
Account with an Impossible travel event (Medium or High confidence)
Step up verification, review against your sharing policy
Account with an Account takeover event (Medium or High confidence)
Treat as possible takeover: force re-auth with the Login and 2FA step-up guide
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 Device ID and webhook payload feed all three, so once you have this wired you can branch on intent.
Next: Acting on results
The full decision playbook, including how to combine the account’s spread with the Risk Score of each identification and its signals.