> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shieldlabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Risk Scoring

> How ShieldLabs scores each identification from 0 to 100 and how users, devices, visitors and IP addresses take a risk band.

ShieldLabs scores every identification from 0 to 100, and the `signals` array next to the number names every risk signal that produced it. Higher means riskier: more likely masked, spoofed, automated or abusive. Every user, device, visitor and IP address then carries the worst band of its identifications, so one Dangerous signup marks the account. You act on the result in your backend and choose the action for each case: allow, challenge, review or block.

## What the Risk Score is

The Risk Score is one value per identification, with the evidence attached.

* **Range 0 to 100.** The only value above 100 is `999`, the rate-limit marker described under [Caveats](#caveats).
* **Explainable.** Every Risk Score ships with a `signals` array, so you never act on a black box.
* **Delivered, not computed in the browser.** It arrives by [webhook](/api/webhooks) and is readable from the [History API](/api/server-api).

Risk signals can add up past 100; the Risk Score you receive is capped at 100.

## How the Risk Score is built

Each risk signal that fires adds a fixed weight, and the weights are summed under the [combination rules](#how-signals-combine) below, then capped at 100. The webhook `signals` array lists every risk signal that changed the Risk Score as `{ "name": "<slug>", "weight": <int> }`: `name` is a stable slug (`proxy`, `antidetect_browser`) and `weight` is what it added to the Risk Score. Two details matter when you parse it: a slug can repeat with a partial weight when an earlier result for the same device and IP address is carried forward, and `stun_late_correction` carries a negative weight when a late network check cancels `stun_not_checked`. Branch on slugs and `detection_flags`, and read `risk_score` for the total.

The `data` object of a webhook for an identification from an anti-detect browser behind a proxy, shortened:

```json theme={null}
{
  "request_id": "7c2e9a41-5d3b-4f8e-a1c6-0b9d2e4f6a18",
  "user_hid": "a91f3c7e5b2d4086",
  "device_id": "5eb7fd5c-8c2e-4a91-b0f3-1d7c9e2a4b55",
  "visitor_id": "161dfbad-2f4a-4c81-9e0b-7a3c5d8f1e22",
  "public_ip": {
    "ip": "203.0.113.24",
    "country": "DE"
  },
  "os": "Windows",
  "connection_type": "proxy",
  "risk_score": 70,
  "signals": [
    { "name": "proxy", "weight": 10 },
    { "name": "antidetect_browser", "weight": 60 }
  ],
  "detection_flags": { "proxy": true, "anti_detect_browser": true },
  "observed_at": "2026-09-16T18:00:45Z"
}
```

The Risk Score is `70`: Anti-detect Browser adds 60 and Proxy adds 10, which places the identification in the Dangerous band. Because both reasons are named, you can see that the anti-detect browser drove this result and choose a different action than you would for a proxy alone.

The [identification card](/dashboard/identification-card) in the analytics dashboard shows the same Risk Score and weights:

<Frame caption="The Risk Score of one identification and each risk signal with its weight, in the analytics dashboard.">
  <img className="block dark:hidden" src="https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/identification-score-signals.png?fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=3afb420d98939a5a88c868d7a076c962" alt="The Risk Score gauge at 70.00, Dangerous, and the Risk signals table of one identification in the analytics dashboard: Anti-detect Browser with weight 60 and Proxy with weight 10." width="2238" height="440" data-path="images/dashboard/identification-score-signals.png" />

  <img className="hidden dark:block" src="https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/identification-score-signals-dark.png?fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=f8c03e36f9a68169c9a9710c004f1b2b" alt="The Risk Score gauge at 70.00, Dangerous, and the Risk signals table of one identification in the analytics dashboard in the dark theme: Anti-detect Browser with weight 60 and Proxy with weight 10." width="2238" height="440" data-path="images/dashboard/identification-score-signals-dark.png" />
</Frame>

<Note>
  Webhook `signals[].name` is a stable slug (`proxy`, `antidetect_browser`). Branch on `name` or `detection_flags`. Display labels can change in the analytics dashboard, and `score_details` on the History API holds internal descriptions rather than slugs. A numeric `weight` alone does not tell you which risk signal fired: 30 from `os_not_detected` is not the same case as 30 from `browser_vpn_proxy`. Read the band, the flags, the user's history and the sensitivity of the action together. See [Acting on results](/guides/acting-on-risk-score).
</Note>

## The weights

Every risk signal that adds to the Risk Score carries a fixed weight, published below. This is the scoring contract: each risk signal adds the weight in this table, and the combination rules decide which weights are added. Only two entries differ: a slug carried forward from an earlier result, which can repeat with a partial weight (see above), and the `rate_limited` entry that comes with the 999 marker (see [Caveats](#caveats)). The heavier the weight, the stronger and rarer the tell: Tor or a headless client is a near-certain tell, while a lone VPN or proxy is common and often legitimate, so it carries far less.

| Risk signal | Slug (`signals[].name`) | Weight | What it means |
| - | - | -: | - |
| Tor | `tor` | 99 | Connection exits through the Tor network |
| JavaScript Disabled | `javascript_disabled` | 90 | The browser lacks capabilities every ordinary browser has, which marks a headless or automated client |
| OS Mismatch | `os_mismatch` | 60 | The operating system the browser reports is inconsistent with other evidence about the device |
| Anti-detect Browser | `antidetect_browser` | 60 | Anti-detect or fingerprint-spoofing indicators are present |
| Anti-detect browser, proxy-routed | `proxy_routed_antidetect` | 60 | Anti-detect browser indicators seen through a proxy; added only when Anti-detect Browser has not fired |
| Browser Automation | `browser_automation` | 60 | The browser is driven by an automation framework |
| STUN not Checked | `stun_not_checked` | 30 | The network check did not complete |
| OS not Detected | `os_not_detected` | 30 | The OS could not be derived from the available signals |
| Browser VPN/Proxy | `browser_vpn_proxy` | 30 | An in-browser VPN or proxy extension was detected |
| VPN | `vpn` | 15 | Connection through a VPN |
| Privacy Relay | `privacy_relay` | 15 | iCloud Private Relay or a similar relay |
| Proxy | `proxy` | 10 | The IP is flagged as a proxy |
| Datacenter IP | `datacenter_ip` | 10 | The IP belongs to a datacenter or hosting range |
| Abuser Flag | `abuser` | 10 | The IP address has a record of abuse in IP reputation data |
| Timezone Mismatch | `timezone_mismatch` | 10 | The device timezone does not match the IP geolocation |
| Late network check | `stun_late_correction` | -30 | A correction: cancels STUN not Checked when the network check completes after the first Risk Score |

<Info>
  The first column is the display label; the second is the slug you receive in `signals[].name`. Branch on the slug or on `detection_flags`, and treat `score_details` on the History API as an internal log: it holds internal descriptions and debug entries, so store it for review but never branch on it or show it to your users. Flag keys match the slugs except for Anti-detect Browser, whose flag is `detection_flags.anti_detect_browser`; the proxy-routed anti-detect and late network check rows have no flag. The same weight from two different risk signals is not interchangeable. IP Mismatch, Incognito, Search bot, Suspicious Paid Click and Check Incomplete are [informational flags](/features/risk-signals#informational-flags) with a weight of 0.
</Info>

## How signals combine

The Risk Score is additive, with these rules applied before the sum:

* **Tor and Privacy Relay are exclusive.** When either fires, no other network, operating system or anti-detect weight is added next to it. JavaScript Disabled and Browser Automation still add.
* **VPN and Browser VPN/Proxy stand in for the proxy stack.** Each replaces the Proxy, Datacenter IP, Abuser Flag, OS Mismatch, OS not Detected, STUN not Checked and Timezone Mismatch weights with its own. Anti-detect Browser still adds on top of a VPN.
* **Anti-detect indicators do not stack.** One Anti-detect Browser weight is added, however many indicators fire.
* **Proxy, Datacenter IP and Abuser Flag stack** with each other when neither VPN nor Browser VPN/Proxy fires.
* **Known search-engine crawlers score 0.** The identification is set to 0 and carries the `search_bot` flag.
* **JavaScript Disabled** (90) on its own places an identification in the Dangerous band.

## How to use the Risk Score

The Risk Score maps to three bands. The payload carries only the number (`risk_score` on the webhook, `score` in History), with no band field, so map the number to a band in your backend. The recommended action is a guide: pick the action per band that fits your risk tolerance.

| Band | Range | Meaning | Recommended action |
| - | - | - | - |
| **Trusted** | 0-29 | No meaningful risk signals, or one minor risk signal | Allow, no friction |
| **Suspicious** | 30-59 | Several overlapping risk signals, or one moderate risk signal | Step-up challenge, second look, or review |
| **Dangerous** | 60-100 | Strong risk signals | Block, review, or require verification |

ShieldLabs returns the Risk Score and every named risk signal on each identification, and detects High-Risk Events on your users. You choose the action for each case (allow, step up, review or block) and act on the result in your backend. ShieldLabs stops fraud and abuse and helps block fraudulent and abusive traffic.

## Risk of a user, device, visitor or IP

Only an identification has a number. A user, device, visitor or IP address carries a band: the worst band among its identifications in the period you look at. A user with twenty Trusted identifications and one Dangerous identification is Dangerous, so one masked or automated signup is enough to mark the account. [Users, devices, visitors and IPs](/concepts/entities) explains how identifications link to each identity.

High-Risk Events are a separate axis. ShieldLabs detects multi-accounting, account sharing, impossible travel and account takeover on your users, each at Medium or High confidence, and a user can be Trusted on every identification and still be detected for multi-accounting. High-Risk Events are available in the analytics dashboard, the API and webhooks. See [High-Risk Events](/features/high-risk-events).

To read an account's band in your backend, fetch its identifications from the [History API](/api/server-api#read-every-identification-of-one-account) and take the highest `score`, skipping values above 100:

```bash theme={null}
curl "https://account.shieldlabs.ai/api/v1/history/user_hid/a91f3c7e5b2d4086?limit=100" \
  -H "Authorization: Bearer sec_your_private_api_key"
```

The key reads one domain; page with `offset` when an account has more than 100 identifications. Search by `device_id`, `visitor_id` or `ip` the same way for a device, a visitor or an address.

In the analytics dashboard, every user, device, visitor and public IP card shows this band for the selected period, with the split of its identifications across Trusted, Suspicious and Dangerous ([User, device, visitor and IP cards](/dashboard/entity-card)).

<Frame caption="A user in the analytics dashboard: the worst band of its identifications and a High-Risk Event (red pill: High confidence).">
  <img className="block dark:hidden" src="https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/user-card-head.png?fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=908ca639023658efff51166ce2abe922" alt="The header of the user card for User HID a91f3c7e5b2d4086 in the analytics dashboard: the Dangerous band pill, a red Multi-accounting pill (High confidence) and the band split of 12 identifications: 10 Trusted, 1 Suspicious, 1 Dangerous." width="2254" height="434" data-path="images/dashboard/user-card-head.png" />

  <img className="hidden dark:block" src="https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/user-card-head-dark.png?fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=ed4591dc1fa5262b6c40bd08949186f3" alt="The header of the user card for User HID a91f3c7e5b2d4086 in the analytics dashboard in the dark theme: the Dangerous band pill, a red Multi-accounting pill (High confidence) and the band split of 12 identifications: 10 Trusted, 1 Suspicious, 1 Dangerous." width="2254" height="434" data-path="images/dashboard/user-card-head-dark.png" />
</Frame>

## Caveats

<Warning>
  Read a high Risk Score together with its named risk signals and the user's history. A real customer on a corporate VPN can reach the Suspicious band; the risk signals show why, and you choose the action for each case. Decide on **the Risk Score, its risk signals, the user's band, any High-Risk Event on the user, and the context** (signup, login, payment, withdrawal), never on the number alone. Tune your cut-points gradually as you observe real traffic.
</Warning>

The risk scale is **0 to 100**. The value **999** is the rate-limit marker, written when a visitor IP is banned after too many identifications: it is not a 0 to 100 Risk Score and not a band. A rate-limit ban also surfaces as [HTTP 429](/rate-limits) on the identification request. The 999 marker can arrive in `risk_score` on the webhook and in `score` on the History API, so treat any value above 100 as that marker before you read the band. On the webhook the marker comes with one `signals` entry, `{ "name": "rate_limited", "weight": 999 }`: skip it when you log or sum risk signals.

## Next steps

<CardGroup cols={2}>
  <Card title="Acting on results" icon="code" href="/guides/acting-on-risk-score">
    Turn the Risk Score, its risk signals and the user's band into allow, challenge, review or block in your backend.
  </Card>

  <Card title="Risk Signals" icon="list-check" href="/features/risk-signals">
    What each risk signal covers, from masking to bots and automation, and how it is surfaced.
  </Card>

  <Card title="Users, devices, visitors and IPs" icon="users" href="/concepts/entities">
    How each identification links to a user, a device, a visitor and IP addresses, each with its own risk.
  </Card>

  <Card title="Accuracy" icon="bullseye" href="/features/accuracy">
    How ShieldLabs measures 99.9% identification accuracy and 99.9% risk signal detection accuracy.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.