Skip to main content
ShieldLabs scores. Your code decides. The Risk Score (0 to 100) and its signals arrive on your server by webhook and are readable from the Server 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 drive decisions off the score band and the action context, the honest caveats, and the implementation notes that keep your decisions correct under at-most-once webhook 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.

The principle: Score plus signals 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: The Risk Score is explainable: every webhook ships with a signals array of { "name": "<name>", "weight": <int> } entries, so you can see and log the reasons behind a number instead of acting on a black box. (The History API exposes the same breakdown.) Drive the decision off the Risk Score band, the numeric weight of each signal, and which anonymity signals fired (read the stable detection_flags, not the free-form signal label string, which is not a stable contract). Read the Risk Score for how the score is built and its weights, and Anonymity Signals for what each signal means.

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.
Default ladder (Node.js)
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. 999 is a rate-limit ban marker, not a 0–100 score, but it can still arrive in the score field (risk_score on the webhook, score on the History API), so guard the value > 100; see the Implementation notes below.

Hard rules on specific signals

The band folds every signal into one number, but sometimes you want to act on a specific tell no matter the score. Branch on the stable detection_flags booleans for that, not the free-form signal labels. The object ships on every webhook:
Flag-level override
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, or KYC — Know Your Customer identity verification). The tables below are recommended starting points; calibrate them against your own data as described in rule 2 below.
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

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.
Signup decision

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 (an extra verification step) 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.
Login decision

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.

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.
Withdrawal decision (strictest)

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.

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.

Driving decisions off the Risk Score band

The Risk Score is the decision input. It already encodes signal severity: a Tor exit carries a far higher weight than a lone VPN, several overlapping mismatches push a visit into the High band, and a stripped or non-cooperating client lands near the top. So a band-based action ladder, tuned per action context, captures the intent of “react harder to stronger anonymity” without you having to inspect each anonymity signal by hand. Two visits with the same total can still differ, and the lever for that is action context plus your own data, layered on top of the band:
  • Raise the stakes, lower the threshold. On money-movement (checkout, withdrawal) react in the Low band already; on a low-stakes action you can wave a Medium-band visit through. The per-scenario tables above set those cut-points as a recommendation.
  • A high score on a sensitive step is a hard look. A Medium or High score at a payment, withdrawal, or password reset warrants a step-up or a hold, because the score is already telling you the visit looks masked or spoofed.
  • Cross-reference your own data. A low Risk Score plus a DeviceID that matches a known-bad device in your database is still a block. You get the identity and the signals; your own history is the other half of the decision.
The shape of that logic is: check your own data first, then let the per-action band ladder carry the rest. The complete handler below expands it.
Use the signals array to see which anonymity signals fired and the points each added — for the decision, for logging, and for later review. Drive the decision off the Risk Score band, each signal’s numeric weight, and which signals fired (branch on the stable detection_flags, not the free-form signal label string, which is not a stable contract). What each signal means is documented in Anonymity Signals, and the weights in Risk Scoring.

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. 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. Match the response to the band and the action context. A 30 is a Medium-band visit: on a low-stakes action it deserves no friction, while the same 30 at a password reset or a withdrawal is worth a challenge. Let the band plus the stakes of the action set the response, and use the signals to inform it and to log the reasoning.
  2. Tune thresholds gradually, starting in log-only mode. Ship the integration first with no enforcement: record score and signals for every check and watch how your real traffic distributes in the analytics dashboard 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-band visit through on a low-stakes action and to challenge a Low-band visit on a withdrawal. The per-scenario tables above exist precisely so the same Risk 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. At-most-once webhook delivery means a naive handler can miss a result entirely, and applying the same check twice (a webhook plus a History API fallback) can double-apply effects. Wire these in from the start.

Receive via webhook, and verify it

Your decision logic lives in the webhook handler. Verify the X-Shield-Signature header before you trust the payload, then respond 200 fast and run your decision off the request path. The signature recipe, constant-time comparison, and the full Node, Go, and Python handlers are on Webhooks: this page assumes a verified payload and focuses on what you do with it.
Verify first, then act (Node.js / Express)

Be idempotent on request_id

ShieldLabs sends one webhook per scored identification: the server waits up to ~60 seconds for any follow-up network check, then sends the final score once. Still key your apply logic on request_id. For anything you cannot afford to miss you may also read the same result from the History API as a fallback, so making the write idempotent on request_id ensures the webhook and a History read for the same check converge instead of double-applying a business effect.
Idempotent apply

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 Server API History endpoint instead of relying solely on the webhook.
Read the result back by RequestID
History returns a { data, total } envelope with snapshots, newest first. You can also look up recent activity by device_id, visitor_id, ip, user_hid, request_id, session_id, or cookie_id. History reads through account.shieldlabs.ai do not consume request balance. The Server API has the full field list and search types.
The score field is named differently on each surface: risk_score on the webhook data, score on the recommended account.shieldlabs.ai History rows, and Score on the PascalCase Management/debug snapshots (api.shieldlabs.ai). The signal breakdown is signals[{ name, weight }] on the webhook but score_details (a JSON string of [{ Value, Description }]) on the History rows. Normalize both when a decision path can be fed by either the webhook or a History fallback, as the applyDecision sketch above does.
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.

A complete decision handler

Putting the pieces together: verify, cross-reference your own data, drive the verdict off the per-action band ladder, log the signals for review, and stay idempotent.
Full handler sketch (Node.js)

Where to go next

The Risk Score

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

Signals

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

Webhooks

At-most-once 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.