Skip to main content
Each tutorial is an end-to-end pattern: where to call the snippet, what arrives on the webhook (or the API), and how your own code acts on the identity, the Risk Score and its signals, and the dashboard Patterns that surface how one identity connects across many sessions — to prevent fraud and abuse, measure traffic quality, or recognize a trusted returning visitor. ShieldLabs scores the request; your application owns the verdict, so every threshold below lives in your code where you can read it, log it, and tune it. The tutorials share the same building blocks:
  • The snippet collects 100+ signals.
  • A webhook (or a History API read) delivers the six identifiers and the explainable Risk Score (0-100) with its signals.
  • You build your own logic on that score and its signals. The Clean / Low / Medium / High bands are a guide, not a rule.
  • The dashboard Patterns grade an identity over time, surfacing the many-accounts-on-one-device shapes a single request cannot see.
Read Acting on the Risk Score first if you want the decision skeleton these tutorials reuse.

The logic every tutorial follows

Every tutorial is the same four moves; only the action and the thresholds change:
  1. Identify at the action (signup, login, checkout). You get the durable DeviceID, and you pass your own UserHID so the account is tied to the device. Cleared cookies, incognito, and a new IP do not break that link.
  2. Read the anonymity in real time. The Risk Score and its signals tell you whether this session is masked right now.
  3. Count on the device. Several distinct UserHIDs on one DeviceID (or one Local IP) is not abuse on its own — how many is too many depends on your platform (an email provider may expect several accounts behind one device, while a bank expects one account per customer). Your code counts it at the action against your own threshold, and the dashboard Patterns surface the same correlation over time for what one request cannot see.
  4. Act in your own code. Allow, challenge, review, or block at the action, and use Patterns exports as watchlists for offline review. ShieldLabs surfaces the evidence; your code owns every verdict.
New here? Start with the Quickstart to install the snippet and receive your first score, then Acting on the Risk Score for the decision pattern every tutorial below reuses.

Account and access

New Account Fraud

Catch multi-accounting and farm signups by joining the Risk Score with the DeviceID at registration.

Multi-Accounting

Link many accounts back to one person through a shared DeviceID and network, the pattern behind bonus, trial, and loyalty abuse.

Account Takeover

Compare the login DeviceID against the account’s history and step up when a known account hits a new device or country.

Credential Stuffing

Score login anonymity and throttle on the durable DeviceID so rotated IPs stop resetting your limits.

Login and 2FA

Call forceCheckAuthenticatedUser at login and step up to 2FA when the session scores Medium or High.

Account Sharing

See one account spread across many devices and countries, and enforce your own sharing policy.

Ban Enforcement

Key bans to the durable DeviceID so a cleared cookie or a fresh account does not let someone back in.

SMS Pumping

Rate-limit verification SMS on the durable DeviceID so one session cannot flood your messaging bill with OTP toll fraud.

Promotions and rewards

Promo Abuse

Count the accounts and redemptions tied to one device to stop signup-bonus and free-trial farming.

Bonus Abuse

Catch repeat signup and deposit bonuses claimed through duplicate accounts on the same device.

Free-Trial Abuse

Spot new accounts cycling the same device to re-claim free trials and free-tier quotas.

Loyalty Fraud

See points and tier rewards farmed across many linked identities instead of genuine activity.

Affiliate Fraud

Rank referral and promo conversions by anonymous-traffic share so masked clicks do not get paid out.

Sybil Attack

Tie many wallets or identities back to one actor before an airdrop, vote, or quota pays out.

Coupon Abuse

Tie each redemption to the DeviceID to enforce one-per-customer codes and refuse reused single-use coupons.

Payments and content

Checkout

Re-identify right before payment, then challenge or hold orders carrying strong anonymity signals.

Chargeback Dispute

Reconstruct a buyer’s device history into evidence against friendly-fraud chargebacks.

Paywall Enforcement

Meter free views on the DeviceID, which clearing cookies or opening incognito cannot reset.

Regional Pricing

Read the anonymity signals and the IP country to catch VPN-masked region switching before you discount.

Card Testing

Anchor each checkout attempt to the durable DeviceID so your code can throttle card attempts and gate masked sessions.

Traffic and experience

Traffic Quality

Score every visit by source and channel to measure cost per real visitor, not per click.

Returning Visitor

Recognize a clean returning device to cut friction for trusted visitors, the inverse of the fraud checks.

How every tutorial is shaped

1

Identify at the right moment

Load the snippet on the relevant page. For sensitive actions (login, payment, withdrawal) call forceCheckAnonymous or forceCheckAuthenticatedUser to clear the session and re-score on the spot. Always pass a hashed account id (UserHID) to the authenticated calls, never a raw email.
2

Receive the identity and Risk Score

Each scored identification carries the six identifiers — the durable DeviceID among them — alongside the explainable Risk Score and its signals. Most tutorials key on that DeviceID, not the score alone: it is what links the “new” accounts a farm spins up. You receive it on the webhook, or read the same result from the History API by request_id when a webhook is ever missed. The server waits up to ~60 seconds for an optional follow-up network check, then posts the final score once (about a second when no follow-up is expected). Verify X-Shield-Signature on the raw body and make your handler idempotent on request_id.
3

Decide in your own code

Branch on the band, and read each signal’s numeric weight for the action context. A payment that lands in the High band warrants more than the raw number alone. Persist request_id plus your decision so the verdict is auditable.
A typical webhook payload the tutorials act on:
{
  "request_id": "8f1d0c2a-7b3e-4a9c-9d2f-1e6a5b4c3d21",
  "visitor_id": "c4a2e9b1-5f8d-4c3a-8e7b-2a1f0d9c8b76",
  "device_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "user_hid": "8a9f...hashed-account-id",
  "public_ip": { "ip": "203.0.113.42", "country": "US" },
  "os": "Mac OS X",
  "browser": "Chrome",
  "risk_score": 70,
    "signals": [
    { "name": "OS Mismatch", "weight": 60 },
    { "name": "Datacenter IP", "weight": 10 }
  ],
  "observed_at": "2026-06-16T10:00:00Z"
}
Here the score is 70 because two additive signals stack: an OS mismatch (60) and a datacenter IP (10).
The signal text is a free-form display label: it can include extra detail and may differ from the friendly names in the Risk Score weight table, so don’t branch on the exact string. Use the stable detection_flags booleans to act on a specific signal, and weigh what each one means for your case — a 30 from one signal is not the same as a 30 from another.
The shared decision skeleton, in your backend:
// Bands: Clean 0-9, Low 10-29, Medium 30-59, High 60-100.
function actionFor(score, signals) {
  // A rate-limit-banned snapshot can reach you scored 999. Guard the band logic.
  if (score > 100) return "block";

  // The weight any single signal contributed is in its numeric weight field.
  // Use the band, never the human-readable signal label, to drive decisions.
  const topSignal = Math.max(0, ...signals.map((d) => d.weight));

  if (score >= 60) return "challenge"; // High: block, review, or require verification
  if (score >= 30) return "review"; // Medium: step-up challenge or a second look
  if (score >= 10) return topSignal >= 30 ? "review" : "allow_log"; // Low, but a heavy single signal is worth a second look
  return "allow"; // Clean: pass through, no friction
}
The Risk Score is 0-100, hard-capped at 100, in four bands: Clean (0-9), Low (10-29), Medium (30-59), High (60-100). A higher score means more anonymous or masked traffic, not a confirmed verdict. A legitimate visitor can score high (a corporate proxy, a VPN, or a privacy browser), so decide on Score plus signals plus action context, never the number alone, and tune thresholds gradually.
Webhooks are at-most-once with no retries and a roughly 1-second timeout. Make handlers idempotent on request_id, and for guaranteed reads poll the History API instead of relying on a single delivery. Webhook delivery is free. History reads through account.shieldlabs.ai do not consume request balance.

The shared webhook-cache helper

Every tutorial receives the score the same way: verify X-Shield-Signature on the raw body, respond fast, store the result by request_id, and let the request path read it back with a short timeout. This is the canonical scoreCache and waitForScore helper the individual tutorials link to instead of repeating it.
webhook-cache.js
import crypto from 'crypto';
import express from 'express';

const scoreCache = new Map(); // use a shared store or your datastore in production
const SECRET = process.env.SHIELDLABS_WEBHOOK_SECRET; // whsec_… for this endpoint

// Keep the raw body so the bytes you hash match the bytes that were signed.
const app = express();
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));

// Webhook handler: verify, respond fast, then store keyed by request_id.
app.post('/shieldlabs/webhook', (req, res) => {
  const received = req.get('X-Shield-Signature') ?? '';
  const expected =
    'sha256=' +
    crypto.createHmac('sha256', SECRET).update(req.rawBody).digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(received);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).end();
  }

  const payload = req.body; // the webhook body, after signature verify
  // Idempotent on request_id: a redelivery can repeat it, so storing is safe
  // but do not double-apply business effects.
  res.status(200).end();
  // Cache the raw payload, so every tutorial reads the same shape the webhook
  // delivers: risk_score, signals, device_id, user_hid, public_ip, local_ip,
  // detection_flags, and the rest.
  scoreCache.set(payload.request_id, payload);
  setTimeout(() => scoreCache.delete(payload.request_id), 5 * 60 * 1000);
});

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// Read path: poll the cache briefly, then fall back to a History API read.
async function waitForScore(requestId, timeoutMs) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (scoreCache.has(requestId)) return scoreCache.get(requestId);
    await sleep(100);
  }
  // Webhook never arrived in time: read the stored snapshot from the History API.
  const [snapshot] = await shieldHistory('request_id', requestId, 1);
  return snapshot ?? null;
}

const HISTORY_BASE = process.env.SHIELDLABS_API_URL ?? 'https://account.shieldlabs.ai/api';
const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_… from dashboard API tab

/** Read snapshots by identifier. Returns the `data` array from the History API. */
async function shieldHistory(searchType, value, limit = 20) {
  const url = new URL(`${HISTORY_BASE}/v1/history/${searchType}/${encodeURIComponent(value)}`);
  url.searchParams.set('limit', String(limit));
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${API_KEY}` },
  });
  if (!res.ok) throw new Error(`history lookup failed: ${res.status}`);
  const body = await res.json();
  return body.data ?? [];
}

Where to go next

If you have not wired up the snippet and a webhook yet, start with the Quickstart and Setup. To understand what the score and its anonymity signals mean, read Risk Score, Signals, and the dashboard Patterns. The API overview has the full payloads and endpoints behind these tutorials.