# Identify Users and Add Context

Out of the box, Shield shows each visit as a session with an IP, a device
fingerprint, a risk score, and a recommendation. It does not know which of
*your* users that session belongs to. This page shows how to tell it, so a
high-risk session can be traced back to an account in your system.

## How it works

You give the SDK the visitor's user ID, email, and plan. Nothing is sent as
given. In the browser (WebCrypto), before anything leaves the page, the SDK:

1. hashes the user ID and the lowercased email with SHA-256, and
2. encrypts them with your site's identity key — an RSA-OAEP public key that
   Shield generates for every site and that the SDK fetches once per page.

Every event then carries these fields:

| You provide | Shield receives |
|---|---|
| `userId` | `user_id_hash` — SHA-256 of the ID, and `user_id_enc` — the ID encrypted with your site's key |
| `email` | `email_hash` — SHA-256 of the lowercased address, `email_domain` (e.g. `gmail.com`), and `email_enc` — the address encrypted with your site's key |
| `plan` | `plan`, as given |
| `salt` (optional) | mixed into both hashes as `SHA-256(salt + ':' + value)` |
| `custom` (optional, SDK 1.1.1+) | `custom`, as given — account facts such as `account_tier` or `signup_channel`, same rules as the `custom` object of `track()` below; shown in the dashboard's Visitor section. Never put PII here |

The ciphertext can only be opened by the Shield dashboard, which holds your
site's private key (sealed at rest, never sent to the browser or stored next
to your events in the clear). Shield's ingest and storage only ever see the
hashes, the domain, the plan, and the ciphertext — never a plain email or
user ID. Sites where identity reveal is switched off, and pages served over
plain HTTP (no WebCrypto), send the hashes only, as SDK 1.0.9 did.

Requires SDK 1.1.0 or later for the encrypted fields (1.0.9 for hashes);
the `v1.js` CDN URL always serves the latest. Nothing changes in your
snippet: the SDK fetches the key itself from
`https://shield.findip.net/v1/shield/identity-key`. Pass `identityKey` to
`init` (or `data-identity-key` on the script tag) with the key shown in
your site's Settings to skip that request.

## Pick your install method

### Google Tag Manager, official template (no code)

Open the FindIP Shield tag and expand **Identify the visitor**. Select the
variables your site already exposes for logged-in users, typically the Data
Layer Variables you use for GA4's `user_id`:

| Field | What to select |
|---|---|
| User ID | e.g. `{{DLV - userId}}` |
| Email address | e.g. `{{DLV - userEmail}}` |
| Plan | e.g. `{{DLV - plan}}` or a constant |
| Hash salt | an optional secret string |

If your site does not expose a user ID variable yet, ask your developer to
push one to the dataLayer for logged-in users; it is the same variable GA4
uses.

### Google Tag Manager, Custom HTML tag

Add an `identify` option to the `init` call with your GTM variables. GTM
substitutes `{{Variable}}` references inside Custom HTML before the tag runs:

```js
window.FindIP.init({
  siteKey: 'pub_xxxxxxxxx',
  identify: {
    userId: '{{DLV - userId}}',
    email: '{{DLV - userEmail}}',
    plan: '{{DLV - plan}}',
    custom: { account_tier: '{{DLV - accountTier}}', seats: '{{DLV - seats}}' }
  }
});
```

Numeric variables render as strings inside Custom HTML; the SDK turns
numeric strings back into numbers.

Unset variables render as `undefined` and are ignored. The dashboard's
Install page generates the full tag with the "Identify logged-in visitors"
toggle enabled.

### npm

```ts
import { init, identify } from '@findip/shield';

init({
  siteKey: 'pub_xxxxxxxxx',
  identify: {
    userId: currentUser.id,
    email: currentUser.email,
    plan: currentUser.plan,
  },
});

// Logged in after page load? Call identify() any time; pass null on logout.
identify({ userId: user.id, email: user.email });
```

### Script tag

Render the values into `data-*` attributes for the logged-in user:

```html
<script
  src="https://cdn.findip.net/shield/v1.js"
  data-site-key="pub_xxxxxxxxx"
  data-user-id="12345"
  data-user-email="jane@example.com"
  data-plan="pro">
</script>
```

An optional `data-hash-salt` attribute sets the salt. For pages where the user
logs in without a reload, call `FindIP.identify({ ... })` after login.

### WordPress, WooCommerce, and Shopify

The official plugins send coarse page-type context only (product view,
checkout view, order received) and never user identifiers. To identify users
on those platforms, use the GTM template if you also run GTM, or call
`FindIP.identify({ ... })` from your theme for logged-in users.

## More context per event

Beyond identity, `FindIP.track()` accepts business context on individual
events. Identity set via `identify` is merged in automatically; fields passed
to `track()` win when both are present.

```js
FindIP.track('checkout_started', {
  account_age_days: 412,
  transaction_amount: 129.0,
  currency: 'USD',
  lead_source: 'google_ads',
  custom: { cart_items: 3, coupon_applied: true }
});
```

| Field | Type | Accepted values |
|---|---|---|
| `user_id_hash`, `email_hash` | string | hash-shaped: hex 32–128 chars or base64 32+ chars. Set automatically by `identify`; pass directly only if you hash server-side |
| `user_id_enc`, `email_enc` | string | `fk1.<key id>.<base64>` ciphertext under the site's identity key. Set automatically by `identify`; anything else is dropped |
| `email_domain` | string | domain only, e.g. `gmail.com` |
| `plan`, `lead_source`, `form_name` | string | up to 256 characters |
| `transaction_amount`, `account_age_days` | number | any finite number |
| `currency` | string | ISO 4217 code, uppercase, e.g. `USD` |
| `custom` | object | up to 20 keys, key names up to 64 characters, values are strings up to 256 characters, numbers, or booleans |

Any other field is dropped. Inside `custom`, keys whose names contain
`password`, `card`, `cvv`, `ssn`, `secret`, and similar are dropped, and any
string value that looks like an email address, phone number, or card number is
dropped, wherever it appears.

Sites with a dataLayer can also push `user_id_hash`, `email_hash`,
`email_domain`, `plan`, `transaction_amount`, `currency`, `form_name` and
`lead_source` before the SDK loads; automatic events pick up the first value
found for each key. Values set through `identify` take precedence.

## Where the identity appears

Open the site in the Shield dashboard. **Events** and **Sessions** both have a
**Visitor** column showing the email (and user ID) of identified visitors;
the event and session drawers have a **Visitor** section with the email, user
ID, plan, domain and hashes. Sessions show the identity of their latest
identified event, so a session that logged in halfway through is labelled
too.

The **Show visitor emails and user IDs** switch in the site's **Settings**
(on by default) controls the whole feature: switched off, the SDK stops
sending encrypted values, anything already stored stays encrypted and the
dashboard shows only the domain and hash. To find the account behind a hash,
compute the same SHA-256 (with the same salt) of the value in your own system.

The rest of the context is under **Raw payload (sanitized)** in the event
drawer as `customer_context`. Fields that were dropped are shown as `null`,
which is the quickest way to check that a value passed validation.

## Troubleshooting

- **`user_id_hash` is `null` in the payload.** Either the value you passed was
  empty or `undefined`, or the page is not served over HTTPS. WebCrypto is only
  available in secure contexts; without it the SDK still sends `plan` and
  `email_domain` but skips the hashes and the encrypted values.
- **The Visitor column shows only `@domain` or a hash.** The event carried no
  encrypted identity (SDK older than 1.1.0, plain-HTTP page, or "Show visitor
  emails and user IDs" was off when the event was sent), or the switch is off
  now. Check the site's Settings and the SDK version on the Install page.
- **Identity missing on the first event.** You called `FindIP.identify()`
  after the page events had already been sent. Pass `identify` to `init`
  instead, or set `data-*` attributes on the script tag.
- **A `custom` value is missing.** It was longer than 256 characters, matched
  a sensitive pattern, or the object already had 20 keys.
- **Debugging live.** Add `debug: true` to the `init` options (or
  `data-debug="true"` on the script tag) to log each outgoing event and the
  resolved identity keys to the browser console.

---

Canonical page: https://www.findip.net/docs/shield/identify-users · Markdown: https://www.findip.net/docs/shield/identify-users.md · All Shield docs in one file: https://www.findip.net/llms-full.txt
