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

# Install the JS Snippet

> Learn how to install the JavaScript snippet and start identifying visitors.

The ShieldLabs client is a single **ES module** you load by dynamic `import()` from `cdn.shieldlabs.ai`. There is no npm package and no native mobile SDK. It runs in the browser only.

The module collects 100+ browser, device, and network signals and posts them to ShieldLabs automatically. It does **not** compute a VisitorID, DeviceID, or Risk Score in the browser. Those are derived on the server and delivered by [webhook](/setup/webhooks) and the [Server API](/api/server-api). You correlate the browser call to the server result using the `requestID` from the optional callback.

<Note>
  The snippet stores a long-lived first-party id under the key **`cookieID`** in both `localStorage` and a first-party cookie. That value is sent to ShieldLabs as `cookieID` and stored server-side as **CookieID**. **VisitorID** is computed on the server from the DeviceID and CookieID and is not written to browser storage.
</Note>

## Install (HTML)

Add this near the top of `<body>`. Use `type="module"` (the snippet relies on `import.meta.url` and top-level dynamic import, so it cannot run as a classic script).

```html theme={null}
<script type="module">
  const mod = await import('https://cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_PUBLIC_KEY');
  mod.checkAnonymous();
</script>
```

`publicKey` is your site's public key, one per registered domain. It is safe to expose in page source. Grab yours from the **Snippet** tab (see [Keys](/setup/keys)).

<Note>
  The call is fully **async and non-blocking**. Several checks run in parallel and never block page render. The Promise resolves when the snapshot POST completes; the identification result arrives separately (see below).
</Note>

## The four exports

The module exports four functions. They all run a full identification and bill one request per call; they differ only in whether you tag a user id and whether they reset the visit session first.

| Export                                            | What it does                                             | Use it for                                                      |
| ------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------- |
| `checkAnonymous(userHID?, callback?)`             | Identifies an untagged visitor                           | A visitor you cannot identify yet (logged-out traffic)          |
| `checkAuthenticatedUser(userHID, callback?)`      | Tags the visit with your UserHID                         | A logged-in user. Pass a hashed/pseudonymous id                 |
| `forceCheckAnonymous(callback?)`                  | Resets the visit session first, then identifies          | Before a sensitive action when you need a fresh anonymous check |
| `forceCheckAuthenticatedUser(userHID, callback?)` | Resets the visit session first, tagged with your UserHID | Right after login, or before a high-risk action                 |

<Warning>
  Always pass a **hashed or pseudonymous** id to `checkAuthenticatedUser` and `forceCheckAuthenticatedUser`. Never pass a raw email or a real account id. ShieldLabs stores this value as your UserHID to correlate activity. It should not be reversible to a real identity.
</Warning>

<Note>
  The account-based [Patterns](/features/patterns) (Many Accounts on One Device, Many Devices on One Account, and the rest keyed on accounts) only populate when you pass a UserHID through `checkAuthenticatedUser`. If you only ever call `checkAnonymous`, the device, visitor, and Local IP patterns still work, but the account-keyed ones cannot form.
</Note>

### `forceCheck*`: clear the session and run now

For `checkAnonymous` and `checkAuthenticatedUser`, the short-lived visit window only governs the SessionID: calls inside the same window share one SessionID, while a call after it expires gets a fresh one. The work itself (collecting signals and posting the snapshot for scoring) always runs.

`forceCheckAnonymous` and `forceCheckAuthenticatedUser` do the same full identification, but they reset the visit session first so this call starts a new SessionID. Reach for them when the moment matters:

* **Right after login.** Re-run the check now that you know who the user is, so the server links this session to the account.
* **Before a sensitive action** (checkout, withdrawal, password change, a new device approval). Get a fresh score keyed to a `requestID` you can act on.

```js theme={null}
// after a successful login
mod.forceCheckAuthenticatedUser(hashedUserId, (ip, requestID) => {
  // store requestID, then read the score server-side via webhook or History API
});
```

## The optional callback

Each export accepts an optional callback as its **last** argument. It fires once, after the snapshot POST resolves. The callback always comes after the user id slot, so for an anonymous call pass `undefined` first:

```js theme={null}
mod.checkAnonymous(undefined, (ip, requestID) => {
  // ip: the client IP the server saw for this call (not a score)
  // requestID: the UUID for this call. Your join key to the server result.
});
```

<Warning>
  The browser does **not** return the VisitorID, DeviceID, or Risk Score. Those are computed on the server. Send the `requestID` to your backend, then read the result from the [webhook](/setup/webhooks) payload or the [History API](/api/server-api) (query by `request_id`).
</Warning>

The flow:

1. The snippet collects signals and POSTs them to `rest.shieldlabs.ai`.
2. Your callback fires with `(ip, requestID)`.
3. The server scores asynchronously, waiting up to \~60s for optional follow-up network checks, then delivers **one** webhook with the final score (\~1s when no follow-up is expected).
4. Your backend matches the webhook (or History API row) to the browser call by `requestID`.

## Framework integrations

The HTML method above works anywhere. In a framework, put the same dynamic `import()` inside a lifecycle hook so it runs once on mount. Pass your public key in from config or props.

<Tabs>
  <Tab title="Native JS">
    ```html theme={null}
    <script type="module">
      const PUBLIC_KEY = 'YOUR_PUBLIC_KEY';
      const mod = await import(`https://cdn.shieldlabs.ai/snippet.js?publicKey=${PUBLIC_KEY}`);

      // anonymous visitor:
      mod.checkAnonymous();

      // or a logged-in user (hashed id only):
      // mod.checkAuthenticatedUser('user_hashed_id_here');

      // force an immediate check (clears session), e.g. right after login:
      // mod.forceCheckAuthenticatedUser('user_hashed_id_here');
    </script>
    ```
  </Tab>

  <Tab title="React">
    ```jsx theme={null}
    import { useEffect } from 'react';

    export function ShieldlabsTracker({ publicKey, hashedUserId }) {
      useEffect(() => {
        let cancelled = false;

        async function run() {
          const mod = await import(
            `https://cdn.shieldlabs.ai/snippet.js?publicKey=${publicKey}`
          );
          if (cancelled) return;

          if (hashedUserId) {
            mod.checkAuthenticatedUser(hashedUserId);
          } else {
            mod.checkAnonymous();
          }
        }

        run();
        return () => { cancelled = true; };
      }, [publicKey, hashedUserId]);

      return null; // renders nothing
    }
    ```
  </Tab>

  <Tab title="Angular">
    ```ts theme={null}
    import { Injectable } from '@angular/core';

    @Injectable({ providedIn: 'root' })
    export class ShieldlabsService {
      // memoize so the module loads only once across the app
      private modulePromise?: Promise<any>;

      async check(publicKey: string, hashedUserId?: string) {
        if (!this.modulePromise) {
          this.modulePromise = import(
            /* @vite-ignore */
            `https://cdn.shieldlabs.ai/snippet.js?publicKey=${publicKey}`
          );
        }
        const mod = await this.modulePromise;

        if (hashedUserId) {
          mod.checkAuthenticatedUser(hashedUserId);
        } else {
          mod.checkAnonymous();
        }
      }
    }
    ```
  </Tab>

  <Tab title="Vue">
    ```vue theme={null}
    <script setup>
    import { onMounted } from 'vue';

    const props = defineProps({
      publicKey: { type: String, required: true },
      hashedUserId: { type: String, default: '' },
    });

    onMounted(async () => {
      const mod = await import(
        `https://cdn.shieldlabs.ai/snippet.js?publicKey=${props.publicKey}`
      );

      if (props.hashedUserId) {
        mod.checkAuthenticatedUser(props.hashedUserId);
      } else {
        mod.checkAnonymous();
      }
    });
    </script>

    <template>
      <div style="display:none"></div>
    </template>
    ```
  </Tab>

  <Tab title="Preact">
    ```jsx theme={null}
    import { useEffect } from 'preact/hooks';

    export function ShieldlabsTracker({ publicKey, hashedUserId }) {
      useEffect(() => {
        let cancelled = false;

        async function run() {
          const mod = await import(
            `https://cdn.shieldlabs.ai/snippet.js?publicKey=${publicKey}`
          );
          if (cancelled) return;

          if (hashedUserId) {
            mod.checkAuthenticatedUser(hashedUserId);
          } else {
            mod.checkAnonymous();
          }
        }

        run();
        return () => { cancelled = true; };
      }, [publicKey, hashedUserId]);

      return null;
    }
    ```
  </Tab>

  <Tab title="Svelte">
    ```svelte theme={null}
    <script>
      import { onMount } from 'svelte';

      export let publicKey;
      export let hashedUserId = '';

      onMount(async () => {
        const mod = await import(
          `https://cdn.shieldlabs.ai/snippet.js?publicKey=${publicKey}`
        );

        if (hashedUserId) {
          mod.checkAuthenticatedUser(hashedUserId);
        } else {
          mod.checkAnonymous();
        }
      });
    </script>
    ```
  </Tab>

  <Tab title="WordPress">
    Save as `wp-content/plugins/shieldlabs-tracker/shieldlabs-tracker.php`, activate in **Plugins**, then visit any front-end page. The snippet runs from `wp_footer` as an ES module — do not enqueue it with `wp_enqueue_script` (classic scripts cannot load the module).

    **Anonymous visitors**

    ```php theme={null}
    <?php
    /**
     * Plugin Name: ShieldLabs Tracker
     * Description: Loads the ShieldLabs snippet for anonymous visitors.
     * Version: 1.0.0
     */

    defined('ABSPATH') || exit;

    const SHIELDLABS_PUBLIC_KEY = 'YOUR_PUBLIC_KEY';

    add_action('wp_footer', 'shieldlabs_track_anonymous', 20);

    function shieldlabs_track_anonymous(): void {
      $snippet_url = 'https://cdn.shieldlabs.ai/snippet.js?publicKey=' . rawurlencode(SHIELDLABS_PUBLIC_KEY);
      ?>
      <script type="module">
        const mod = await import(<?php echo wp_json_encode($snippet_url); ?>);
        mod.checkAnonymous();
      </script>
      <?php
    }
    ```

    **Logged-in users** — hash the WordPress user id; never pass email, login, or the raw numeric id.

    ```php theme={null}
    <?php
    defined('ABSPATH') || exit;

    const SHIELDLABS_PUBLIC_KEY = 'YOUR_PUBLIC_KEY';

    add_action('wp_footer', 'shieldlabs_track_authenticated', 20);

    function shieldlabs_track_authenticated(): void {
      if (!is_user_logged_in()) {
        return;
      }

      $user = wp_get_current_user();
      $user_hashed_id = hash('sha256', (string) $user->ID . wp_salt('auth'));
      $snippet_url = 'https://cdn.shieldlabs.ai/snippet.js?publicKey=' . rawurlencode(SHIELDLABS_PUBLIC_KEY);
      ?>
      <script type="module">
        const mod = await import(<?php echo wp_json_encode($snippet_url); ?>);
        mod.checkAuthenticatedUser(<?php echo wp_json_encode($user_hashed_id); ?>);
      </script>
      <?php
    }
    ```

    <Note>
      If you use full-page cache (WP Rocket, LiteSpeed Cache, etc.), either exclude logged-in sessions from cache or use one combined plugin that picks `checkAnonymous` vs `checkAuthenticatedUser` per request. After login, call `forceCheckAuthenticatedUser` from a small inline script on the post-login page if you need an immediate re-check.
    </Note>
  </Tab>

  <Tab title="Tilda">
    **Site-wide:** Tilda → **Site Settings** → **Code insertion** → **HTML code for insertion inside HEAD** → **Edit code** — paste the snippet and republish.

    **One page:** add block **T123** (Other → HTML) at the bottom of the page.

    **Anonymous visitors**

    ```html theme={null}
    <script type="module">
      const mod = await import('https://cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_PUBLIC_KEY');
      mod.checkAnonymous();
    </script>
    ```

    **Logged-in users (Tilda Members or custom login)** — pass a hashed id only. Tilda does not expose a stable member id in the page by default; set it yourself after login (`localStorage` or `window.__shieldlabsUserHid`), then run the check on member pages:

    ```html theme={null}
    <script type="module">
      const mod = await import('https://cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_PUBLIC_KEY');

      const userHashedId =
        window.__shieldlabsUserHid ||
        localStorage.getItem('shieldlabs_user_hid');

      if (userHashedId) {
        mod.checkAuthenticatedUser(userHashedId);
      } else {
        mod.checkAnonymous();
      }
    </script>
    ```

    <Note>
      Tilda hosts the site — you normally do not configure CSP yourself. After editing site HTML, use **Publish all pages** so the code appears on every page.
    </Note>
  </Tab>

  <Tab title="Shopify">
    **Online Store → Themes → ⋯ → Edit code → `layout/theme.liquid`** — paste before `</body>`, then **Save**.

    **Anonymous visitors**

    ```liquid theme={null}
    {% assign shieldlabs_public_key = 'YOUR_PUBLIC_KEY' %}
    {% assign shieldlabs_snippet_url = 'https://cdn.shieldlabs.ai/snippet.js?publicKey=' | append: shieldlabs_public_key %}
    <script type="module">
      const mod = await import({{ shieldlabs_snippet_url | json }});
      mod.checkAnonymous();
    </script>
    ```

    **Logged-in customers** — hash the Shopify customer id; never pass email, name, or the raw numeric id.

    ```liquid theme={null}
    {% if customer %}
      {% assign user_hashed_id = customer.id | append: shop.permanent_domain | sha256 %}
      {% assign shieldlabs_public_key = 'YOUR_PUBLIC_KEY' %}
      {% assign shieldlabs_snippet_url = 'https://cdn.shieldlabs.ai/snippet.js?publicKey=' | append: shieldlabs_public_key %}
      <script type="module">
        const mod = await import({{ shieldlabs_snippet_url | json }});
        mod.checkAuthenticatedUser({{ user_hashed_id | json }});
      </script>
    {% endif %}
    ```

    **Both flows in one block** (recommended for most themes):

    ```liquid theme={null}
    {% assign shieldlabs_public_key = 'YOUR_PUBLIC_KEY' %}
    {% assign shieldlabs_snippet_url = 'https://cdn.shieldlabs.ai/snippet.js?publicKey=' | append: shieldlabs_public_key %}
    <script type="module">
      const mod = await import({{ shieldlabs_snippet_url | json }});
      {% if customer %}
        {% assign user_hashed_id = customer.id | append: shop.permanent_domain | sha256 %}
        mod.checkAuthenticatedUser({{ user_hashed_id | json }});
      {% else %}
        mod.checkAnonymous();
      {% endif %}
    </script>
    ```

    <Note>
      `theme.liquid` runs on the **Online Store** only. Shopify **Checkout** is a separate host with its own strict CSP — use server-side checks (order webhooks, Shopify Flow, or a custom app) for checkout fraud, not this snippet.
    </Note>
  </Tab>
</Tabs>

<Tip>
  Memoize the import (the Angular example caches `modulePromise`) so the module loads once even if you mount the wrapper in several places. The framework wrappers are thin: they call the same CDN module as the HTML method, just from your app code instead of an inline script.
</Tip>

## Capturing the result with a callback

Pass a callback in any framework to grab the `requestID` and hand it to your backend:

```jsx theme={null}
useEffect(() => {
  let cancelled = false;

  async function run() {
    const mod = await import(
      `https://cdn.shieldlabs.ai/snippet.js?publicKey=${publicKey}`
    );
    if (cancelled) return;

    mod.checkAuthenticatedUser(hashedUserId, async (ip, requestID) => {
      // forward requestID to your server, which reads the score from the
      // webhook or the History API and decides allow / challenge / review / block.
      await fetch('/api/shieldlabs/checked', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ requestID }),
      });
    });
  }

  run();
  return () => { cancelled = true; };
}, [publicKey, hashedUserId]);
```

ShieldLabs surfaces the score and signals. Your own code owns the decision, and [acting on the Risk Score](/guides/acting-on-risk-score) shows how to turn the score and its `signals` into an allow / challenge / review / block path in your application.

## Next steps

<CardGroup cols={2}>
  <Card title="Content Security Policy" icon="shield-halved" href="/setup/csp">
    Allowlist the ShieldLabs snippet hosts if your site sends a strict CSP header.
  </Card>

  <Card title="API Keys" icon="key" href="/setup/keys">
    Find your domain's public key for the snippet, and the private API key and secret key for the server.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/setup/webhooks">
    Receive the scored result keyed by requestID, one webhook per identification.
  </Card>

  <Card title="How it works" icon="diagram-project" href="/">
    Follow a single identify call from the browser snapshot to the server score.
  </Card>
</CardGroup>
