> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shieldlabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Account Sharing

> Learn how to detect and prevent account sharing: spot one login used across many devices and countries, and enforce your seat or 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 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](/features/identification) and grades how far an account has spread. Four layers answer four different questions:

| Layer                   | What it answers                                                                | Where you read it                                                                          | Latency               |
| ----------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | --------------------- |
| **Identification**      | "Is this the same device, and is it a new device or country for this account?" | The durable `device_id` on the [webhook](/setup/webhooks) / [History API](/api/server-api) | About a second        |
| **Anonymity detection** | "Is this session masked or anonymous right now?"                               | The `signals` array on the [webhook](/setup/webhooks) / [History API](/api/server-api)     | About a second        |
| **Risk Score**          | "How risky is the visit overall, as one 0-100 number?"                         | `risk_score` on the [webhook](/setup/webhooks) / [History API](/api/server-api)            | About a second        |
| **Patterns**            | "Has this account spread across many devices or countries over time?"          | [Dashboard Patterns](/features/patterns) + export                                          | Background (\~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.

<Note>
  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.
</Note>

## 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)](/features/risk-scoring); your code owns the limit and the verdict. The steps below wire it up.

## Build it

<Steps>
  <Step title="Create a ShieldLabs account and get your keys">
    [Sign up for free](https://app.shieldlabs.ai/) 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](/setup/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](/api/server-api), and each webhook endpoint has its own `whsec_…` signing secret. See [Keys](/setup/keys).
  </Step>

  <Step title="Identify authenticated sessions">
    Add the [snippet](/setup/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.

    ```html app.html theme={null}
    <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>
    ```
  </Step>

  <Step title="Check the device on every session">
    Read the scored result for that `RequestID` from your webhook cache (the shared [`waitForScore` helper](/use-case)), or fall back to the [History API](/api/server-api), then compare the DeviceID against the account's known devices.

    ```js api/session-check.js theme={null}
    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.

    <Warning>
      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](/features/identification) explains. Weigh it with the country spread, the IP, and your own context before you treat it as sharing.
    </Warning>
  </Step>

  <Step title="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](/features/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.

    <AccordionGroup>
      <Accordion title="Many Devices on One Account" icon="users">
        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).
      </Accordion>

      <Accordion title="Multiple Countries on One Account" icon="earth-americas">
        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.
      </Accordion>

      <Accordion title="New Device and New Country" icon="location-dot">
        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.
      </Accordion>
    </AccordionGroup>

    Read these on the [dashboard Patterns tab](/features/patterns), or reconstruct the same counts live from the [History API](/api/server-api):

    ```js Count devices and countries per account theme={null}
    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 });
    }
    ```

    <Note>
      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.
    </Note>
  </Step>

  <Step title="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.
  </Step>
</Steps>

## 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.

## Recommended starting policy

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                                                                          |
| "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](/use-case/step-up-authentication) step-up pattern |

A sudden device-and-country jump on an existing account can be sharing, but it can also be [account takeover](/use-case/account-takeover) or the tail of a [credential-stuffing run](/use-case/credential-stuffing). The same per-session DeviceID and webhook payload feed all three, so once you have this wired you can branch on intent.

<Card title="Next: Acting on the Risk Score" icon="arrow-right" href="/guides/acting-on-risk-score">
  The full decision playbook, including how to combine identity spread with the per-session Risk Score and its signals.
</Card>
