Skip to main content
ShieldLabs scores. Your code decides. The Risk Score (0 to 100) and its Details arrive on your server by webhook and are readable from the Management API. What happens next, allow / challenge / review / block, is logic you write in your own application. There is no in-product rules engine and nothing blocks on its own. This page is the recommendation toolkit: a default band-to-action ladder, per-scenario thresholds you can copy, how to act on specific Details even at a moderate score, the honest caveats, and the implementation notes that keep your decisions correct under two-phase delivery.
Treat everything here as a starting point, not a rule. The 0 to 100 scale is fixed. Where you draw the action line is yours, and the right line depends on how costly a wrong allow or a wrong block is for the action in front of you. Start permissive, watch your own outcomes, then tighten.

The principle: Score plus Details plus action context

The number alone is never the decision. A Risk Score of 65 on a blog comment and a 65 on a $5,000 withdrawal are the same number and completely different situations. Make every decision from three inputs:

Score

The 0 to 100 total. A fast read on how anonymous or abusive the visit looks. Higher means more anonymous (more likely masked, spoofed, or abusive), not a confirmed verdict.

Details

The signals that fired and the points each added. This is the explainability. A 30 from one privacy-relay signal is very different from a 30 built out of several mismatches.

Action context

What the user is doing right now (signup, login, payment, withdrawal) and what a wrong decision costs you. The same score warrants different friction at different stakes.
The Risk Score is explainable: every score ships with a Details array of { "Value": <int>, "Description": "<signal>" } entries, so you can branch on the score, on a single signal, or on a combination of signals. Read the Risk Score for how the score is built and Signals for what each Description means and its weight.

The default band-to-action ladder

ShieldLabs maps every score into one of four bands. These labels are fixed; the recommended action per band is a sensible default you should adapt per scenario below.
BandRangeWhat it meansA reasonable default action
Clean0–9No meaningful signals firedPass through, no friction
Low10–29One minor signalAllow, worth logging
Medium30–59Several overlapping signals, or one moderate signalStep-up challenge, second look, or review
High60–100Strong anonymity or abuse signalsBlock, review, or require verification
Default ladder (Node.js)
// A baseline ladder. Tune the cut-points per action context (see scenarios below).
function defaultAction(score) {
  if (score < 10) return 'allow';        // Clean: no friction
  if (score < 30) return 'allow_log';    // Low: allow, but log it
  if (score < 60) return 'challenge';    // Medium: step-up or review
  return 'review_or_block';              // High: block, review, or verify
}
The bands come straight from the score. Clean 0–9, Low 10–29, Medium 30–59, High 60–100. They are the only band labels ShieldLabs uses. There is no fifth “Critical” band, and 999 is never a customer score (it is an internal rate-limit marker; see Implementation notes).
The cost of a mistake changes with the action, so the threshold should too. Be lenient where a wrong block annoys a real user but costs little (a blog comment), and strict where a wrong allow moves money or grants trust (a withdrawal, KYC). The tables below are recommended starting points. Calibrate them against your own conversion and chargeback data.
A useful mental rule: the higher the cost of a wrong allow, the lower you set the friction threshold. Low-stakes actions can tolerate a Medium score; money-movement should react in the Low band already.

Signup and registration

The most common entry point for multi-accounting, promo and bonus abuse, and account farms. You usually have nothing else to go on yet, so the score and signals carry the decision. Be generous in the Clean and Low bands so you do not tax real users, and reserve hard friction for clear High-band anonymity.
ScoreBandRecommended action at signup
0–9CleanAllow, create the account, no friction
10–29LowAllow, log the signals for later correlation
30–59MediumAdd friction: require email verification or a CAPTCHA
60–100HighReject or hold for manual review before the account is usable
Signup decision
function onSignup({ Score, Details }) {
  if (Score >= 60) return 'reject_or_manual_review';
  if (Score >= 30) return 'email_verify_or_captcha';
  return 'allow'; // Clean / Low: create the account
}

Login and 2FA

At login you already have an account and its history, so you can be a little more permissive on the raw score and lean harder on step-up authentication you already own. A Medium score is a strong reason to require a second factor; reserve a block for High-band signals or an account-takeover pattern.
ScoreBandRecommended action at login
0–29Clean / LowAllow the login
30–59MediumRequire 2FA / step-up authentication
60–100HighBlock the session and require recovery or verification
Login decision
function onLogin({ Score }) {
  if (Score >= 60) return 'block_require_recovery';
  if (Score >= 30) return 'require_2fa';
  return 'allow';
}

Checkout and payment

Money is moving, so the threshold drops. Anonymity signals on a payment (proxy, Tor, a VPN that does not match the saved billing region) deserve a hard look earlier than they would at signup. Add friction in the Medium band and gate the High band behind verification.
ScoreBandRecommended action at checkout
0–9CleanAllow
10–29LowAllow, log; watch combined with order value
30–59MediumStep-up: 3-D Secure, extra verification, or hold for review
60–100HighBlock the charge or require manual review before fulfillment

Withdrawal and high-value action

The strictest scenario. A wrong allow here is an irreversible loss, so react to signals you would wave through elsewhere. Add verification as early as the Low band, and route the High band to a human.
ScoreBandRecommended action at withdrawal
0–9CleanAllow
10–59Low / MediumRequire extra verification (2FA, cooldown, or a confirmation step)
60–100HighHold for manual review; do not auto-approve
Withdrawal decision (strictest)
function onWithdrawal({ Score, Details }) {
  if (Score >= 60) return 'manual_review_hold';
  if (Score >= 10) return 'extra_verification'; // verify early on money out
  return 'allow';
}

KYC gating

Use the score to decide who must complete identity verification before they get a sensitive capability, not to make the identity decision itself. A Medium or High score is a strong reason to require full KYC up front rather than letting the user defer it.
ScoreBandRecommended action for KYC gating
0–29Clean / LowStandard onboarding; KYC on your normal schedule
30–59MediumRequire KYC before enabling the sensitive capability
60–100HighRequire full KYC and hold the capability until it passes

Content, comment, and posting

The most lenient scenario. A wrong block costs you a real contributor; a wrong allow costs you a spam comment you can remove later. Keep friction low and only react meaningfully in the High band.
ScoreBandRecommended action for content/posting
0–29Clean / LowPublish normally
30–59MediumAllow but rate-limit, queue for moderation, or shadow-review
60–100HighHold for moderation or require a verified account to post

Details-aware decisioning

The score is a useful first cut, but the Details array is where precision lives. Two scores of 35 can mean different things, and acting on the specific signals lets you react correctly even at a moderate total. Branch on Description strings, on combinations, and on the action context together. A few patterns worth wiring in:
  • An abuse-reputation hit deserves attention even at a moderate score. If Abuser Flag is present, treat the visit as high-risk regardless of the total, because the device or IP is already on an abuse list.
  • Anonymity on money-movement escalates. Tor or Proxy on a payment or withdrawal is a strong reason to challenge or block, even if the rest of the visit looks ordinary.
  • Combinations beat any single signal. A Timezone Mismatch together with a VPN signal is a more meaningful escalation than either one alone, especially at signup or password reset.
  • Cross-reference your own data. A low score from ShieldLabs plus a DeviceID that matches a known-bad device in your database is still a block. ShieldLabs gives you the identity and the signals; your own history is the other half of the decision.
Details-aware escalation
function decide({ Score, Details, UserHID, action }) {
  const fired = new Set(Details.map((d) => d.Description));
  const has = (name) => fired.has(name);

  // 1. Abuse-reputation hit: high-risk even at a moderate score.
  if (has('Abuser Flag')) return 'block_or_manual_review';

  // 2. Strong anonymity on money-movement: escalate hard.
  const moneyAction = action === 'checkout' || action === 'withdrawal';
  if (moneyAction && (has('Tor') || has('Proxy'))) return 'block_or_manual_review';

  // 3. Combination at a sensitive step: VPN + timezone mismatch.
  if (has('Timezone Mismatch') && hasAnyVpn(fired) && isSensitive(action)) {
    return 'challenge';
  }

  // 4. Cross-reference YOUR own data, even on a low score.
  if (knownBadDevice(/* DeviceID from the payload */)) return 'block';

  // 5. Otherwise fall back to the per-action band ladder.
  return bandLadder(Score, action);
}

function hasAnyVpn(fired) {
  return fired.has('VPN') || fired.has('Browser VPN/Proxy') || fired.has('Privacy Relay');
}
The signal names you branch on (Tor, Proxy, VPN, Abuser Flag, Timezone Mismatch, OS Mismatch, Anti-detect Browser, and the rest) and their weights are documented in Signals. Match the Description strings you actually receive in your payload; treat the signal list as open, since new signals can be added over time.

Honest caveats (read before you tune)

A legitimate user can score high. A corporate VPN, an enterprise proxy, iCloud Private Relay, or a privacy-focused browser all raise the score on a perfectly real person. The score measures how anonymous the traffic looks, not whether the user is bad. Never decide on the number alone. Weigh Score plus Details plus action context every time, and reserve hard blocks for high-stakes actions where a real-user false positive is worth the protection.
Three rules that keep you out of trouble:
  1. Never auto-block on the raw number. Always read the Details. A 30 from a single Privacy Relay signal is a privacy-conscious user; a 30 built from a timezone mismatch plus a VPN at password reset is worth a challenge. Same number, different decision.
  2. Tune thresholds gradually, starting in log-only mode. Ship the integration first with no enforcement: record Score and Details for every check and watch how your real traffic distributes against your conversion and chargeback data. Only then turn on friction, starting with the highest-stakes actions, and tighten in small steps.
  3. Match friction to stakes. It is fine to wave a Medium score through on a blog comment and to challenge a Low score on a withdrawal. The per-scenario tables above exist precisely so the same score earns different friction.
ShieldLabs is the detection layer. It surfaces the identity and the signals; your application owns the verdict. The subject of every “block”, “challenge”, and “allow” is your code, not ShieldLabs.

Implementation notes: getting it right

The recommendations above only hold if your handler reads the data correctly. Two-phase delivery and at-most-once webhooks mean a naive handler can double-apply effects or miss a result entirely. Wire these in from the start.

Receive via webhook, and verify it

Your decision logic lives in the webhook handler. Verify the signature before you trust the payload: the envelope is { "Data": { ... }, "Assing": "<hex hmac-sha256>" }, where Assing is hex( HMAC-SHA256( key = your Secret Key, msg = JSON(Data) ) ) computed over the Data object only. Recompute it and compare in constant time. Full details are in Webhooks.
Verify, then decide (Node.js / Express)
import express from 'express';
import crypto from 'crypto';

const SECRET = process.env.SHIELDLABS_SECRET;
const app = express();

app.post('/shieldlabs/webhook', express.json(), async (req, res) => {
  const { Data, Assing } = req.body;

  // 1. Verify the HMAC over Data before trusting anything.
  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(JSON.stringify(Data))
    .digest('hex');

  const ok =
    Assing.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(Assing, 'hex'), Buffer.from(expected, 'hex'));

  if (!ok) return res.status(401).end();

  // 2. Respond fast, then act.
  res.status(200).end();
  applyDecision(Data).catch(console.error);
});

Be idempotent on RequestID

The webhook can arrive more than once. Phase: "initial" is the first score (about 1 second after the browser check, before the WebRTC real-IP check). Phase: "update" arrives after WebRTC with a recomputed score, and its Details carry only the changed signals. A redelivery can also repeat a RequestID. Use RequestID as your idempotency key so an update or a repeat never double-applies a business effect.
Idempotent apply
async function applyDecision(data) {
  const { RequestID, Score, Details, UserHID, Phase, action } = data;

  // Upsert keyed on RequestID: initial, update, or redelivery all converge.
  await db.checks.upsert({ requestID: RequestID, score: Score, details: Details, phase: Phase });

  // Compute the decision idempotently; do not re-charge, re-email, or re-block.
  const decision = decide({ Score, Details, UserHID, action });
  await db.decisions.upsert({ requestID: RequestID, decision });

  // On the "update" Phase, you may revise an earlier "initial" decision
  // (the WebRTC pass can raise or lower the score). Revise, do not duplicate.
}

Fall back to the History API

Webhook delivery is at-most-once: a single attempt, no retries, with a roughly 1 second timeout. Do not assume at-least-once. For any decision you must not miss (a withdrawal, a payout, a KYC gate), read the result back from the Management API History endpoint instead of relying solely on the webhook.
Read the result back by RequestID
curl "https://api.shieldlabs.ai/myshop.com:YOUR_SECRET_KEY/history/request_id/13f84f05-2c4a-4d8e-9b1a-6f2e7c9d0a55?limit=1"
History returns an array of snapshots, newest first. You can also look up recent activity by device_id, visitor_id, ip, or user_hid to pull a user’s history before deciding. The History API bills one request per returned row (an empty result still bills one), so query narrowly. See the Management API for the full field list and search types.
The HTTP 429 rate-limit response and the internal ban marker are infrastructure protections, not scores. A customer-facing Risk Score is always 0 to 100. If you see a request rejected with 429, that is the per-IP rate limit (a DDoS protection), not a high-risk verdict. See Rate limits.

A complete decision handler

Putting the pieces together: verify, branch on Details first, fall back to the per-action band ladder, and stay idempotent.
Full handler sketch (Node.js)
function bandLadder(score, action) {
  // Per-action cut-points from the scenario tables above.
  const cuts = {
    signup:     { challenge: 30, block: 60 },
    login:      { challenge: 30, block: 60 },
    checkout:   { challenge: 30, block: 60 },
    withdrawal: { challenge: 10, block: 60 }, // strictest: react early
    kyc:        { challenge: 30, block: 60 },
    content:    { challenge: 30, block: 60 },
  }[action] || { challenge: 30, block: 60 };

  if (score >= cuts.block) return 'block_or_manual_review';
  if (score >= cuts.challenge) return 'challenge';
  return 'allow';
}

function decide({ Score, Details, UserHID, action }) {
  const fired = new Set((Details || []).map((d) => d.Description));

  // Details-aware overrides first.
  if (fired.has('Abuser Flag')) return 'block_or_manual_review';
  if ((action === 'checkout' || action === 'withdrawal') &&
      (fired.has('Tor') || fired.has('Proxy'))) {
    return 'block_or_manual_review';
  }

  // Then the per-action band ladder.
  return bandLadder(Score, action);
}

Where to go next

The Risk Score

How the 0 to 100 score is built, what Details contains, and the band definitions.

Signals

Every signal you can branch on, in plain language, with weights and combination rules.

Webhooks

Two-phase delivery, the full payload, signature verification, and idempotency.

Cookbook

End-to-end recipes for login and 2FA, checkout, signup, affiliate fraud, and traffic quality.
Ready to apply this to a specific flow? The cookbook has worked examples: Login and 2FA, Checkout, Account signup, Affiliate fraud, and Traffic quality.