# FindIP Shield documentation, complete > Every public FindIP Shield doc in one file, for AI assistants. Each section is also served on its own at https://www.findip.net/docs/shield/{slug}.md; the HTML pages are at the same URLs without .md. Index: https://www.findip.net/llms.txt # FindIP Shield documentation FindIP Shield adds visitor risk intelligence to a website: VPN, proxy, Tor, relay, hosting, datacenter, malicious-IP and network-rotation signals on visits, sessions and form submits, with a risk score, the reasons behind it, and a response you configure per form. It never collects passwords, payment fields, form values, raw emails or phone numbers. Free Preview: 10,000 events per site per day. Every page below is available as Markdown by appending `.md` to its URL, or by requesting the HTML URL with `Accept: text/markdown`. All pages in one file: https://www.findip.net/llms-full.txt For coding agents installing Shield into a project: https://www.findip.net/docs/shield/agent-install.md ## Install - [Quickstart](https://www.findip.net/docs/shield/quickstart.md): npm, script tag, or pinned build with SRI. - [JavaScript SDK reference](https://www.findip.net/docs/shield/javascript-sdk.md): `init`, `track`, `identify`, `getSession`, `setConsent`, every option. - [Google Tag Manager](https://www.findip.net/docs/shield/google-tag-manager.md): official tag template, dataLayer events. - [WordPress](https://www.findip.net/docs/shield/wordpress.md) and [WooCommerce](https://www.findip.net/docs/shield/woocommerce.md): the plugin and the checkout context it adds. - [Shopify](https://www.findip.net/docs/shield/shopify.md): the App Store app and its Web Pixel limits. - [Cloudflare Workers integration](https://www.findip.net/docs/shield/cloudflare.md): no-code edge monitoring and optional blocking for one hostname. ## Use the data - [Events reference](https://www.findip.net/docs/shield/events.md): standard events and payload fields. - [Identify users and add context](https://www.findip.net/docs/shield/identify-users.md): hashed user IDs and allowlisted business context. - [In-page enforcement](https://www.findip.net/docs/shield/enforcement.md): per-form responses (monitor, slow down, Turnstile, stop, redirect) and how outcomes are recorded. - [Server verification](https://www.findip.net/docs/shield/server-verification.md): the verify endpoint, the one decision a browser cannot bypass. ## Privacy and terms - [Privacy modes](https://www.findip.net/docs/shield/privacy-modes.md) - [Data collection](https://www.findip.net/docs/shield/data-collection.md) - [Cookies](https://www.findip.net/docs/shield/cookies.md) - [Threat Network](https://www.findip.net/docs/shield/threat-network.md) - [Free Preview terms](https://www.findip.net/docs/shield/terms-free-preview.md) ## Product pages - Overview: https://www.findip.net/shield/overview - Example dashboard with sample data: https://www.findip.net/shield/demo - npm: https://www.npmjs.com/package/@findip/shield · Source: https://github.com/findip-net/findip-shield-js --- Canonical page: https://www.findip.net/docs/shield · Markdown: https://www.findip.net/docs/shield.md # Install FindIP Shield: recipe for coding agents This page is written for an AI coding agent (Claude Code, Cursor, Copilot, Codex or similar) that has been asked to add FindIP Shield to a project. It is deterministic: detect, choose one route, make the smallest change, verify, stop. A human reads the result; do not do more than the steps say. FindIP Shield is a browser SDK plus dashboard that scores website visits and form submits for VPN, proxy, Tor, relay, hosting, datacenter and malicious-IP signals. It reports; enforcement is configured later by the site owner. It never collects passwords, payment fields, form values, raw emails or phone numbers. ## 0. What you need from the user - A **public site key** (`pub_...`). It comes from the Shield dashboard after creating a site for the domain: https://www.findip.net/shield (sign-in required). If the user has no key, stop and ask for one; do not invent or hard-code a placeholder in committed code. - Optionally a **secret key** (`sec_...`) for server verification (step 4). It must only ever live in server-side configuration. Store the public key in the project's existing configuration mechanism (environment variable, config file, CMS setting). Suggested names: `FINDIP_SHIELD_SITE_KEY` (server), `NEXT_PUBLIC_FINDIP_SHIELD_SITE_KEY` / `VITE_FINDIP_SHIELD_SITE_KEY` (bundler-exposed), `FINDIP_SHIELD_SECRET_KEY` (server only, never exposed). ## 1. Detect the project and pick one route Check, in order, and take the first match: | If the project is | Route | Go to | |---|---|---| | A Shopify theme or store | Do not add code. Tell the user to install the app from https://apps.shopify.com/findip-shield and paste the site key there. | stop | | A WordPress site (wp-config.php, wp-content/) | Do not add code. Tell the user to install the "FindIP Shield" plugin from WordPress.org and paste the site key in its settings. WooCommerce context is added automatically. | stop | | Managed through Google Tag Manager only (no app code access) | Use the GTM template: https://www.findip.net/docs/shield/google-tag-manager.md | stop | | A bundled JavaScript app (package.json with React, Next.js, Vue, Nuxt, SvelteKit, Astro, Vite, Angular) | **npm package** | step 2A | | Server-rendered or static HTML (Razor, Blade, Django, Rails, Jinja, PHP, plain HTML) | **script tag** | step 2B | Shield runs in the browser. Do not import it in server-only code, API routes, edge functions or build scripts. ## 2A. npm route ```bash npm install @findip/shield ``` Initialise once, in client-side code that runs on every page after the app mounts. The package exports `init`, `track`, `identify`, `getSession`, `setConsent` and `version`, as ESM and CommonJS with TypeScript types. ```ts import { init } from '@findip/shield'; init({ siteKey: process.env.NEXT_PUBLIC_FINDIP_SHIELD_SITE_KEY!, // or import.meta.env.VITE_..., or your config privacyMode: 'balanced', // 'strict' | 'balanced' | 'advanced'; keep the default unless told otherwise autoTrack: true, // page views autoDetectForms: true, // signup, login, checkout and lead forms by their attributes }); ``` Where to put it: - **Next.js App Router**: a small `'use client'` component rendered once in `app/layout.tsx`, calling `init` inside `useEffect`. - **Next.js Pages Router**: `useEffect` in `pages/_app.tsx`. - **React + Vite**: `main.tsx` before `createRoot`, or `useEffect` in the root component. - **Vue / Nuxt**: a client-only plugin (`plugins/findip-shield.client.ts` in Nuxt). - **SvelteKit**: `onMount` in the root `+layout.svelte`. - **Astro**: a ` ``` `v1.js` always serves the latest non-breaking 1.x build. If the project pins third-party scripts or uses a strict Content Security Policy, use the pinned build with subresource integrity instead; the current hash is on the site's Install page in the dashboard and in the quickstart: ```html ``` If the site has a CSP, allow `script-src https://cdn.findip.net` and `connect-src https://shield.findip.net`. The CDN build auto-initialises from `data-*` attributes and exposes `window.FindIP` with the same methods as the npm package. Script-tag options: `data-site-key` (required), `data-privacy-mode`, `data-auto-track`, `data-auto-detect-forms`, `data-push-to-data-layer`, `data-session-field`, `data-link-session`, `data-debug`. ## 3. Verify the install 1. Run the site and load a page in a browser. 2. In DevTools, Network tab, filter for `shield/track`. A `POST` to `https://shield.findip.net/v1/shield/track` after page load means the SDK is sending. 3. The event appears on the site's dashboard at https://www.findip.net/shield moments later, and the Install page there confirms the first event. If nothing is sent: the domain must be allowlisted for the site key (dashboard, site settings), the origin must match, and in `strict` privacy mode no visitor cookie is expected. Report to the user what was changed, where the key is read from, and that the site is now in monitor-only mode: nothing is enforced until they configure a response per form in the dashboard. ## 4. Optional: server verification (only if asked, or if the task is to protect signup, login or payment) The browser result is informational. For a decision that matters, the server verifies the session with the secret key before acting. The browser SDK can carry the session ID into your request: - Form posts (SDK 1.11.0+): `init({ ..., sessionField: true })` (or `data-session-field="true"`) adds a hidden input `findip_session` to forms that POST to the page's own origin. - Fetch/XHR: `const { sessionId } = getSession();` and include it in the request body. Server side, before creating the account, issuing the session token or capturing payment: ```http POST https://shield.findip.net/v1/shield/sessions/verify Authorization: Bearer Content-Type: application/json { "session_id": "", "event": "signup_attempt" } ``` Response fields to act on: `verified` (boolean), `risk_status` (`available` or `unknown`), `risk_score` (0 to 100), `recommendation` (`allow`, `monitor`, `challenge`, `block`), `challenge_passed` (true once the visitor completed a Turnstile challenge Shield verified), and `session.network` flags (`vpn`, `proxy`, `tor`, `relay`, `hosting`, `datacenter`, `malicious`). Suggested minimal policy, unless the user specifies one: reject `block`; accept `challenge` only when `challenge_passed` is true; treat `risk_status: unknown` as `monitor`, never as safe; log everything else and let it through. Keep the secret key out of client code and out of version control. Full reference: https://www.findip.net/docs/shield/server-verification.md ## 5. Do not - Do not block or redirect visitors from code because a flag such as `vpn` is true. A risk score is an assessment, not proof of abuse; per-form responses are configured by the site owner in the dashboard, not hard-coded. - Do not send form values, passwords, emails, phone numbers or card data to `track()`. To attach a logged-in user, use `identify({ userId, email, plan })`; the SDK hashes the values in the browser. - Do not put the secret key in client code, in `NEXT_PUBLIC_` / `VITE_` variables, or in the repository. - Do not add the SDK to server-side, build-time or edge code paths. - Do not add a second copy if an install already exists (search for `@findip/shield`, `cdn.findip.net/shield`, or `FindIP.init`). ## References - Quickstart: https://www.findip.net/docs/shield/quickstart.md - SDK reference: https://www.findip.net/docs/shield/javascript-sdk.md - Events: https://www.findip.net/docs/shield/events.md - Data collection and privacy modes: https://www.findip.net/docs/shield/data-collection.md · https://www.findip.net/docs/shield/privacy-modes.md - Everything in one file: https://www.findip.net/llms-full.txt - Package: https://www.npmjs.com/package/@findip/shield · Source: https://github.com/findip-net/findip-shield-js --- Canonical page: https://www.findip.net/docs/shield/agent-install.md # Quickstart Install FindIP Shield on your website in under two minutes. ## npm Install Recommended for React, Next.js, Vue, Vite, and other bundled applications: ```bash npm install @findip/shield ``` ```ts import { init, track } from '@findip/shield'; init({ siteKey: 'pub_xxxxxxxxx', privacyMode: 'balanced', autoTrack: true, autoDetectForms: true, }); await track('signup_attempt', { plan: 'free' }); ``` The package includes ESM, CommonJS, and TypeScript declarations. Replace `pub_xxxxxxxxx` with your public site key from the FindIP dashboard. To attach your own user to each session, pass `identify: { userId, email, plan }` to `init()`; the SDK hashes the values in the browser. The Install page's npm snippet has an "Identify logged-in visitors" toggle that adds this for you; see [Identify Users and Add Context](https://www.findip.net/docs/shield/identify-users.md). ## Script Tag Install Paste this before the closing `` tag: ```html ``` Replace `pub_xxxxxxxxx` with your public site key from the FindIP dashboard. The `v1.js` URL always serves the latest non-breaking 1.x build. If you prefer to pin an exact version with subresource integrity, use the pinned snippet (shown on your site's Install page with a current hash): ```html ``` Pinned URLs are immutable; upgrading means changing the version and hash. To attach your own user to each session with the script tag, enable "Identify logged-in visitors" on the Install page. The snippet then carries the user's ID, email and plan as `data-*` attributes; the SDK hashes them in the browser before sending. See [Identify Users and Add Context](https://www.findip.net/docs/shield/identify-users.md). ## What Happens Automatically 1. SDK loads and auto-initializes from `data-*` attributes 2. A first-party session cookie (`_fip_sid`) is created 3. A `page_view` event is sent to FindIP 4. Form submissions are detected and classified (signup, login, checkout, etc.) 5. Risk results are pushed to `dataLayer` if GTM is present ## Next: Identify Your Users Events are anonymous until you attach your own user identifier. Push a hashed user ID to the dataLayer or pass it to `FindIP.track()` so a risky session can be traced back to an account in your system. See [Identify Users and Add Context](https://www.findip.net/docs/shield/identify-users.md). ## Verify Installation Open browser DevTools → Network tab. Filter for `shield/track`. You should see POST requests after page load. ## Options | Attribute | Default | Description | |-----------|---------|-------------| | `data-site-key` | required | Your public site key | | `data-privacy-mode` | `balanced` | `strict`, `balanced`, or `advanced` | | `data-auto-track` | `true` | Auto page view tracking | | `data-auto-detect-forms` | `true` | Auto form submit detection | | `data-push-to-data-layer` | `true` | Push risk results to GTM dataLayer | | `data-debug` | `false` | Enable console debug logging | ## Troubleshooting - **No events appearing**: Check that your domain is allowlisted for the site key - **CORS errors**: Ensure your origin matches the configured allowed domain - **No visitor cookie**: Expected in `strict` privacy mode --- Canonical page: https://www.findip.net/docs/shield/quickstart · Markdown: https://www.findip.net/docs/shield/quickstart.md · All Shield docs in one file: https://www.findip.net/llms-full.txt # JavaScript SDK Reference ## npm Package ```bash npm install @findip/shield ``` ```ts import { init, track, getSession, setConsent } from '@findip/shield'; init({ siteKey: 'pub_xxxxxxxxx', privacyMode: 'balanced', autoTrack: true, autoDetectForms: true, }); await track('login_attempt'); const { sessionId } = getSession(); ``` The named exports provide the same API as the CDN global documented below. ## Global Object ```js window.FindIP ``` ## Methods ### `FindIP.init(options)` Initialize the SDK. Called automatically when using script tag install. ```js FindIP.init({ siteKey: 'pub_xxxxxxxxx', // required privacyMode: 'balanced', // strict | balanced | advanced autoTrack: true, autoDetectForms: true, pushToDataLayer: true, consentRequired: false, noConsentMode: 'strict', // strict | disabled endpoint: 'https://shield.findip.net/v1/shield/track', debug: false, maxPayloadBytes: 32768, sessionCookieDurationMinutes: 30, visitorCookieDurationDays: 30, sessionField: false, // 1.11.0+: true, or a field name, adds the session ID to your forms as a hidden input linkSession: false, // 1.11.0+: carry the session ID across link clicks for browsers that keep no session cookie }); ``` `linkSession` adds a short-lived `_fip` token to same-origin links at click time, only for visitors whose session cookie does not work; see [Cookies](https://www.findip.net/docs/shield/cookies.md). On the script tag: `data-link-session`. `sessionField` adds a hidden input (`findip_session` by default) carrying the Shield session ID to forms that POST to the page's own origin, for [server verification](https://www.findip.net/docs/shield/server-verification.md). On the script tag: `data-session-field` or `data-session-field="your_name"`. ### `FindIP.track(eventName, context)` Track a custom event with optional customer context. ```js FindIP.track('signup_attempt', { email_domain: 'gmail.com', plan: 'free', user_id_hash: 'abc123...', custom: { referral: 'partner_x' }, }); ``` Allowed context fields: `user_id_hash`, `email_hash`, `email_domain`, `account_age_days`, `plan`, `transaction_amount`, `currency`, `form_name`, `lead_source`, `custom`. See [Identify Users and Add Context](https://www.findip.net/docs/shield/identify-users.md) for value rules, hashing, and the dataLayer path. ### `FindIP.identify(options)` Tell Shield which of your users the visitor is. The SDK hashes `userId` and `email` with SHA-256 in the browser and attaches only `user_id_hash`, `email_hash`, `email_domain` and `plan` to every subsequent event. Pass the same object as the `identify` option of `init()` to have it on the first event, or call it later (after a login); pass `null` on logout. ```js FindIP.identify({ userId: user.id, email: user.email, plan: 'pro', salt: 'optional-secret' }); ``` Script-tag equivalents: `data-user-id`, `data-user-email`, `data-plan`, `data-hash-salt`. Requires 1.0.9+. See [Identify Users and Add Context](https://www.findip.net/docs/shield/identify-users.md). ### `FindIP.getSession()` Returns `{ sessionId, visitorId }`. ### `FindIP.setConsent(consent)` ```js FindIP.setConsent(true); FindIP.setConsent(false); FindIP.setConsent({ security_storage: 'granted', analytics_storage: 'denied', }); ``` ## Payload Example See the [events reference](https://www.findip.net/docs/shield/events.md) for the full payload schema. ## Privacy Notes The SDK never collects passwords, credit card numbers, raw emails, or full form contents. Only metadata (field types, counts, button text) is sent. ## Troubleshooting Enable debug mode: `FindIP.init({ siteKey: '...', debug: true })`. --- Canonical page: https://www.findip.net/docs/shield/javascript-sdk · Markdown: https://www.findip.net/docs/shield/javascript-sdk.md · All Shield docs in one file: https://www.findip.net/llms-full.txt # Events Reference ## Standard Events | Event | Description | |-------|-------------| | `page_view` | Page loaded | | `session_start` | New or resumed session | | `form_view` | Form became visible | | `form_submitted` | Generic form submit (low confidence) | | `signup_view` | Signup page viewed | | `signup_attempt` | Signup form submitted | | `signup_success` | Signup completed | | `login_view` | Login page viewed | | `login_attempt` | Login form submitted | | `login_success` | Login succeeded | | `login_failed` | Login failed | | `password_reset_view` | Password reset page | | `password_reset_attempt` | Password reset submitted | | `checkout_view` | Checkout page viewed | | `checkout_started` | Checkout initiated | | `payment_attempt` | Payment form submitted | | `payment_failed` | Payment failed | | `lead_form_view` | Lead/contact form viewed | | `lead_submitted` | Lead form submitted | | `api_request` | API request event | | `custom` | Custom/manual event | ## Auto-Detection Auto-detected events include: ```json { "auto_detected": true, "confidence": 0.94, "detection_method": "form_fields_button_text_url" } ``` Confidence thresholds: - 0.90–1.00 = very likely - 0.70–0.89 = likely - 0.50–0.69 = maybe - below 0.50 = falls back to `form_submitted` ## Payload Schema ```json { "site_key": "pub_xxx", "sdk": { "name": "findip-shield-js", "version": "1.0.0", "integration": "javascript" }, "event": { "name": "signup_attempt", "timestamp": "...", "source": "auto_form_detect" }, "page": { "url": "...", "path": "/signup", "title": "...", "referrer": "...", "utm": {} }, "session": { "session_id": "sess_xxx", "visitor_id": "vis_xxx" }, "browser": { "user_agent": "...", "language": "en-US", "timezone": "..." }, "form": { "field_count": 5, "has_email_field": true, "has_password_field": true }, "customer_context": { "email_domain": "gmail.com", "plan": "free" }, "privacy": { "mode": "balanced", "consent": { "granted": true, "source": "default" } } } ``` ## API Endpoint ``` POST https://shield.findip.net/v1/shield/track Content-Type: text/plain ``` The SDK sends the JSON payload as `text/plain` so requests stay CORS "simple requests" — no preflight, and `sendBeacon` delivery on page unload always works. --- Canonical page: https://www.findip.net/docs/shield/events · Markdown: https://www.findip.net/docs/shield/events.md · All Shield docs in one file: https://www.findip.net/llms-full.txt # 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 ``` 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..` 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 # In-Page Enforcement Shield scores every visit and recommends `allow`, `monitor`, `challenge` or `block`. With in-page enforcement switched on, the SDK acts on that recommendation the moment a risky visitor submits a sign-up, login, checkout, lead or password-reset form, on any install type: JavaScript snippet, npm, Google Tag Manager, WordPress, WooCommerce or Shopify. Nothing changes in your snippet. The setting travels inside the responses the SDK already receives, so there is no extra request, and the decision itself is made by Shield's servers, never by rules shipped to the browser. Requires SDK 1.2.0 or later; the `v1.js` CDN URL always serves it. ## What it is, and what it is not This is friction. It stops bots and casual abuse that run your page in a real browser, which is most of it. It does not stop anyone who posts to your endpoints directly, disables JavaScript, or edits the page. For the sign-ups, logins and payments that matter, confirm the session on your server with the [verify endpoint](https://www.findip.net/docs/shield/server-verification.md), one call from your backend. It returns the same recommendation, and since SDK 1.2.0 also whether the visitor passed a Turnstile challenge. ## Switch it on Open the site in the Shield dashboard. Under **In the page (SDK)** the enforcement pages are **Protected forms** (which forms Shield protects, and how), **Custom rules** (your own decisions) and the **Enforcement log**. Protection is per form. On **Protected forms**, press **Protect a form** and answer three questions: 1. **Which form?** Pick one of the forms Shield has seen submitted on your site in the last 30 days (page, form id / name / action, how Shield classified it, how often it fired), or add a form by its page path if Shield has not seen it yet. Leaving the id, name and action blank protects every form on that page. 2. **What kind of form is it?** Sign-up, login, checkout and payment, email and contact, password reset, or other form. Shield's guess is preselected. Your answer is also the form's correction: from then on Shield classifies that form the way you said, on the page (SDK 1.6.0) and on ingest for sites pinned to an older SDK. 3. **How should Shield respond?** An *enforcement type*: **Monitor only** (record, never interfere), **Slow down**, **Verify (Turnstile)**, **Block**, or a custom type of your own (see below). The page then lists your protected forms with their activity, the forms Shield has seen but does not protect yet (protect or ignore each one with a click), and your enforcement types. Every protected form can be paused, edited or removed; a paused form keeps its category but is not enforced. The **Protection is on / paused** switch at the top pauses everything at once. Up to 150 forms can be protected per site, 50 per kind. ### Enforcement types An enforcement type says what happens in the page for each of Shield's verdicts: | Shield says | Options | |---|---| | `block` (risky visitor) | **Stop** the submit and show your message · **Redirect** to a URL · Let it through | | `challenge` (suspicious visitor) | **Turnstile check** · **Slow down** · **Stop** · Let it through | | `monitor` (slightly unusual visitor) | Let it through · **Slow down** | `allow` is never touched. The built-in types cover most sites: *Monitor only* (nothing, nothing, nothing), *Slow down* (stop, slow down, nothing), *Verify (Turnstile)* (stop, Turnstile check, nothing) and *Block* (stop, stop, nothing), all with the default texts and a 5-second delay. A custom type sets its own mix, the slow-down delay, a redirect URL and the texts visitors see; create one from the wizard or duplicate a built-in one on the Protected forms page. A type in use cannot be deleted. Different forms can use different types on SDK 1.7.0 (`v1.js` always has it). Sites pinned to an older SDK apply the type most of their protected forms use to every protected form. ### Sites set up before per-form protection Sites that used to protect whole form kinds ("every sign-up form Shield recognises") keep working exactly as before until you choose the forms to keep: the Protected forms page shows the forms Shield saw for those kinds, pre-ticked, and confirming switches the site to per-form protection. Until then, protecting new forms and pausing are unavailable. ## The four actions - **Stop.** The submit is cancelled and your message appears in a small dialog floating over the page (centred, on a dimmed backdrop, so it is seen wherever the form sits; SDK 1.10.0 — earlier SDKs append it under the form). The card has the class `findip-shield-notice`, which you can style; the visitor closes it with its Close button, Escape or a click outside. The page's own submit handlers do not run. The texts for all three visible actions are yours to set: the stop message, the challenge message shown above the Turnstile widget, and the slow-down countdown text, where `{seconds}` is replaced by the remaining seconds (SDK 1.5.0). - **Slow down.** The submit is cancelled, a countdown of the configured number of seconds appears in the same dialog, and the form is submitted once automatically when it reaches zero. Scripted signups that fire many submits per second get exactly one, late. - **Challenge.** A Cloudflare Turnstile widget appears in the dialog. When the visitor completes it, the SDK sends the token to Shield, Shield verifies it with your Turnstile secret, records the pass on the session, and the form submits. The pass lasts for the rest of the session, so the visitor is not challenged again on the next form. Without a Turnstile site key this action falls back to slowing down. - **Redirect.** As soon as Shield's decision for the page is `block`, the visitor is sent to the URL you configure, typically a page that explains the situation and offers a way to contact you. The SDK never redirects from the target page itself, so it cannot loop. Every action is recorded on the event. The Events page shows a badge such as *Blocked*, *Slowed*, *Challenged* or *Challenge passed* next to the event name, so you can see how often enforcement fires and on which forms. ## How forms are matched Paths are matched exactly: lowercase, without query string or fragment. Shield identifies forms by their `id`, `name` or action path (SDK 1.3.0+), as metadata only; field values are never collected. A protected form matches when the page path and every key you gave match. Forms Shield cannot recognise as one of the five kinds are reported as *other forms*; protect them like any other form and pick *Other form* as the kind. Forms you never want classified or protected go on the ignore list (the **Ignore** button under suggestions) and can be taken off it again on the same page. A protected form's name, if you give one, shows next to its kind in Events, the enforcement log and alerts. ## Custom rules Rules let you decide for a specific user, visitor, session, network, country or ASN, or for a submit velocity, regardless of Shield's score. They live on the Custom rules page and apply at ingest, so the SDK's in-page actions and the verify endpoint both honour them. | Match | You enter | Notes | |---|---|---| | User | an email, a user ID, or a SHA-256 hash | Stored as a hash only. Uses the same hashing as `identify` without a salt; salted installs enter the hash. | | Visitor | the visitor ID | From the Sessions page or the log. | | Session | the session ID | | | IP or network | an address or CIDR | IPv4 and IPv6. | | Country | an ISO-2 code | | | ASN | a number | | | Submit velocity | form kind, max, minutes, per visitor or IP | Matches when a submit would be the N-th of that kind in the window. | Rules are independent of the in-page switch: with "Enforce in the page" off, a matching rule still changes the verdict shown in the dashboard, pushed to the dataLayer and returned by the verify endpoint, but nothing is stopped, slowed or challenged in the visitor's browser. The Custom rules page says so prominently while the switch is off. A rule can also override the site's in-page behaviour when it decides: its own action (stop, slow down, challenge, redirect, or nothing in the page), slow-down delay, stop, challenge and slow-down texts, and redirect URL. Blank fields keep the site setting. Requires SDK 1.4.0 (1.5.0 for the challenge and slow-down texts). Each rule carries an action: **block**, **challenge**, **monitor**, or **allow**. A matching rule replaces Shield's recommendation for that event and adds `rule:` to the risk reasons. If several rules match, an **allow** rule wins, otherwise the strongest action does. Rules can expire, and the page shows how many submits each one enforced (stopped, slowed, challenged or redirected); events that merely matched, such as page views, are not counted. The enforcement log offers one-click "Block user", "Block visitor" and "Block IP" for 24 hours, 7 days or permanently. ## The enforcement log The Enforcement log page lists every submit the SDK stopped, slowed, challenged or redirected: when, who (the visitor's email and user ID when identity reveal is on, otherwise the visitor ID), which form on which page, what happened, Shield's recommendation and reasons, and the network. It is the place to check that a rule works and to see what the friction is catching. ## Turnstile setup 1. In your Cloudflare account open **Turnstile** and create a widget for your domain. The free plan is enough. 2. Copy the **site key** and **secret key** into **Settings › Cloudflare Turnstile** in Shield. The site key is shown to visitors; the secret never leaves Shield. 3. Give the forms the **Verify (Turnstile)** enforcement type, or a custom type whose response to `challenge` is the Turnstile check. Shield loads Turnstile only when a challenge is actually needed, so pages that never trigger one do not load it at all. If Cloudflare's verification service cannot be reached, the challenge fails open and the submit goes through; the outcome is recorded as such. ## Fail-open rules The SDK never blocks by accident. It leaves a submit alone when: - enforcement is off, or the page has not yet received Shield's response for this visit (for example a submit within the first few hundred milliseconds), - the recommendation is `allow`, or the form is not in scope, - the visitor already passed a challenge in this session, - a challenge cannot be rendered or verified. Forms submitted from JavaScript with `form.submit()` do not fire a submit event and are therefore not intercepted; `requestSubmit()` and normal button submits are. ## Combining it with your server Put the session ID in a hidden field and call the verify endpoint from your backend before creating the account or taking the payment: ```json { "verified": true, "risk_status": "available", "risk_score": 72, "recommendation": "challenge", "challenge_passed": true, "challenge_passed_at": "2026-09-08T09:12:44.000Z" } ``` A backend that accepts `challenge` only when `challenge_passed` is true, and rejects `block`, turns the in-page friction into a decision no client can bypass. --- Canonical page: https://www.findip.net/docs/shield/enforcement · Markdown: https://www.findip.net/docs/shield/enforcement.md · All Shield docs in one file: https://www.findip.net/llms-full.txt # Server Verification The browser SDK provides signals but cannot be trusted for enforcement. Use server-side verification for security decisions. ## Endpoint ``` POST /v1/shield/sessions/verify Authorization: Bearer sec_xxx ``` ## Request ```json { "session_id": "sess_xxx", "event": "signup_attempt" } ``` ## Response ```json { "verified": true, "risk_status": "available", "risk_score": 87, "recommendation": "challenge", "challenge_passed": false, "challenge_passed_at": null, "session": { "visitor_id": "vis_xxx", "first_seen_at": "2026-09-08T10:00:00.000Z", "last_seen_at": "2026-09-08T10:05:00.000Z", "event_count": 4, "pageview_count": 2, "risk_reasons": ["vpn_detected", "payment_attempt"], "ip_rotating": true, "asn_rotating": false, "country_rotating": false, "distinct_ips": 2, "distinct_asns": 1, "distinct_countries": 1, "ips": ["203.0.113.7", "203.0.113.9"], "countries": ["IL"], "last_ip": "203.0.113.9", "last_country": "IL", "last_country_name": "Israel", "last_asn": 1680, "last_asn_name": "Partner", "last_organization": "Partner Communications", "network": { "vpn": true, "proxy": false, "tor": false, "relay": false, "hosting": false, "datacenter": false, "malicious": false } } } ``` `session` is the picture Shield has of the whole session, for your own records — an order notification, a fraud queue, a CRM note. `risk_reasons` are the distinct reasons across every event; `ip_rotating`, `asn_rotating` and `country_rotating` are true when the session was seen from more than one IP, network or country; `network` flags are true if *any* event of the session came from that kind of address; `last_*` describe the most recent event. `ips` holds at most 10 addresses. Identity is not included — your backend already knows who its customer is. `challenge_passed` is true once the visitor completed a Cloudflare Turnstile challenge that Shield verified for this session (see [In-Page Enforcement](https://www.findip.net/docs/shield/enforcement.md)). A backend that accepts `challenge` only when `challenge_passed` is true, and rejects `block`, turns the in-page friction into a decision no client can bypass. `risk_status` is `unknown` when no event of the session had IP intelligence; treat it as `monitor`, never as safe. ## Recommended Flow 1. Browser SDK tracks `signup_attempt` and receives initial risk signal 2. Your backend receives the signup request with `session_id` 3. Backend calls `/v1/shield/sessions/verify` with secret API key 4. Backend enforces decision (allow, challenge, block) based on verified response ## Why Server Verification - Browser payloads can be spoofed - Secret API keys must never be in client-side code - Server verification provides authoritative risk decisions ## Integration Example ```js // Frontend: include session ID in signup request const { sessionId } = FindIP.getSession(); await fetch('/api/signup', { method: 'POST', body: JSON.stringify({ email, password, findip_session_id: sessionId }), }); ``` ### Plain HTML forms For a form the browser posts itself, let the SDK add the session ID as a hidden field (SDK 1.11.0 and later): ```js FindIP.init({ siteKey: 'pub_xxxxxxxxx', sessionField: true }); ``` Every form that POSTs to your own origin then carries `findip_session=`; read it on the server like any other form field. Pass a string to choose another name. This also works for visitors whose browser blocks cookies, where reading the `_fip_sid` cookie on the server finds nothing. ```js // Backend: verify before creating account const verify = await fetch('https://shield.findip.net/v1/shield/sessions/verify', { method: 'POST', headers: { Authorization: `Bearer ${process.env.FINDIP_SHIELD_SECRET_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ session_id: req.body.findip_session_id, event: 'signup_attempt', }), }); ``` --- Canonical page: https://www.findip.net/docs/shield/server-verification · Markdown: https://www.findip.net/docs/shield/server-verification.md · All Shield docs in one file: https://www.findip.net/llms-full.txt # Privacy Modes ## strict Maximum privacy. Use when consent is denied or regulatory requirements demand minimal collection. - No persistent visitor cookie (`_fip_vid`) - No localStorage - Session-only ID via cookie → sessionStorage → in-memory fallback - Minimal browser context: user agent, language, timezone only - No advanced fingerprinting ## balanced (default) Recommended for most websites. - First-party session cookie (`_fip_sid`, 30 min rolling) - Optional visitor ID (`_fip_vid`, 30 days), kept in a cookie and in `localStorage` - Full page/referrer/UTM context - Form metadata (field types, not values) - Standard browser metadata ## advanced For sites needing stronger repeat-visitor detection. - Longer-lived visitor cookie - Extended browser/device consistency signals - Customer-provided hashed IDs supported - Still no raw sensitive fields ## Consent Integration ```js FindIP.init({ siteKey: 'pub_xxx', consentRequired: true, noConsentMode: 'strict', // or 'disabled' }); ``` When consent is denied: - `noConsentMode: 'strict'` — continues in strict mode - `noConsentMode: 'disabled'` — stops all tracking ## What Is Never Collected - Passwords, credit cards, full form contents - Raw emails, phones, names, addresses - Keystrokes, mouse recordings, screenshots - Full DOM snapshots --- Canonical page: https://www.findip.net/docs/shield/privacy-modes · Markdown: https://www.findip.net/docs/shield/privacy-modes.md · All Shield docs in one file: https://www.findip.net/llms-full.txt # Data Collection > The limits on this page are enforced **server-side as well as in the SDK**: the ingest API > rebuilds every payload from a strict allowlist and rejects sensitive-looking values (raw > emails, phone numbers, card patterns, password-like fields), so bypassing the SDK cannot store > undocumented data. Raw request payloads are never persisted. > > Free Preview sites also contribute pseudonymized, infrastructure-level sightings to the > [FindIP Threat Network](https://www.findip.net/docs/shield/threat-network.md) — network-level data only, identical in every > privacy mode, never including anything from the sections below. ## What the SDK Collects ### Always (all modes) - Event name and timestamp - Page URL, path, title, referrer - UTM parameters - Session ID - SDK version ### balanced / advanced - Visitor ID (cookie) - Browser language, timezone, screen/viewport - Form metadata (field types, counts, button text category) - Customer-provided context (allowlisted fields only) ### advanced only - Extended device consistency signals - Touch support detection ## What the SDK Never Collects - Passwords or payment card numbers - Full form input values - Raw email addresses or phone numbers - Names or physical addresses - Keystrokes, mouse movements, screenshots - Full DOM content ## Form Metadata Example ```json { "field_count": 5, "has_email_field": true, "has_password_field": true, "has_phone_field": false, "has_payment_field": false, "has_message_field": false, "submit_text_type": "signup" } ``` Input **values** are never read or transmitted. ## Customer Context Only these fields are accepted via `FindIP.track()`: - `user_id_hash`, `email_hash` — hashed identifiers - `email_domain` — domain only (e.g. `gmail.com`) - `plan`, `currency`, `transaction_amount` - `form_name`, `lead_source` - `custom` — object with safe string/number/boolean values ## Backend Enrichment The FindIP backend adds (not collected by SDK): - Visitor IP, ASN, geolocation - VPN/proxy/Tor/hosting/malicious flags - Risk score and recommendation --- Canonical page: https://www.findip.net/docs/shield/data-collection · Markdown: https://www.findip.net/docs/shield/data-collection.md · All Shield docs in one file: https://www.findip.net/llms-full.txt # Cookies FindIP Shield stores two random IDs in the visitor's browser, both first-party: a session ID and a visitor ID. Neither contains personal data. ## `_fip_sid` (Session) | Property | Value | |----------|-------| | Purpose | Session continuity | | Duration | 30 minutes (rolling) | | Value | Random opaque ID, e.g. `sess_abcd1234` | | Contains | No IP, email, or personal data | ## `_fip_vid` (Visitor) | Property | Value | |----------|-------| | Purpose | Repeat visitor detection | | Duration | 7–30 days (configurable) | | Disabled in | `strict` privacy mode | | Value | Random opaque ID, e.g. `vis_abcd1234` | | Also stored in | `localStorage`, key `_fip_vid` (SDK 1.11.0 and later) | ### The `localStorage` copy of the visitor ID From SDK 1.11.0 the visitor ID is kept in two places with the same lifetime: the `_fip_vid` cookie and a `localStorage` entry of the same name (`.`). On each page the SDK reads whichever still holds the ID, the cookie first, and writes both again. A visitor whose cookie was removed, or whose browser refuses the cookie, keeps the same visitor ID as long as the `localStorage` entry is there. - Clearing all site data in the browser removes both and starts a new visitor. - In `strict` privacy mode neither is written or read. - When a visitor who had agreed withdraws consent (`FindIP.setConsent(false)` after an earlier grant), the SDK deletes both and stops sending the visitor ID. Agreeing again starts a new one. If your cookie notice lists the storage your site uses, list the `_fip_vid` `localStorage` entry next to the cookie. ## When Cookies Are Blocked Session ID: 1. `sessionStorage` 2. The tab's `window.name` (SDK 1.11.0 and later), see below 3. A link token, only when you switch on `linkSession` (SDK 1.11.0 and later), see below 4. In-memory session ID, for the current page only 5. When nothing in the browser keeps the ID, Shield derives one for the visit from the request (IP address, browser and language, for one UTC day) and the SDK uses it. Nothing is stored in the browser for this. Visitor ID: 1. `localStorage` (not in `strict` mode) 2. When neither the cookie nor `localStorage` can be written, the SDK sends no visitor ID. Shield derives one the same way as the session ID, except in `strict` mode. Sessions from such browsers are marked "Cookies blocked" in the dashboard, with what held the session together instead (for example "Cookies blocked · window name"). The session and event details show it in full under "Recognised by". The SDK continues working without cookies. ### The session ID in `window.name` When the browser refuses both the cookie and `sessionStorage`, the SDK keeps the session ID in the tab's `window.name`, as `_fip_sid=..`. It stays for the pages of one visit in that tab and ends when the tab closes. - It expires like the session cookie (`sessionCookieDurationMinutes`). - It names your site key, so a session written on one site is never read on another. The value is a random ID with no personal data; a page the visitor navigates to next in the same tab may be able to read it, as with any `window.name`. - The SDK only writes an empty `window.name`. If your page or the window that opened it gave the window a name, the SDK leaves it alone and falls back to the next step. - The SDK removes its value once the cookie or `sessionStorage` works again. ### The session ID in a link token (`linkSession`, off by default) `window.name` belongs to one tab, so a link opened in a new tab starts a new session, and a window that already has a name cannot be used at all. With `FindIP.init({ linkSession: true })` the SDK also carries the session ID in the link itself, for visitors whose session cookie does not work: - When the visitor clicks a same-origin link, the SDK adds `_fip=.