> ## 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.

# Troubleshooting

> Fixes for the issues you are most likely to hit while integrating ShieldLabs.

A fast path from a symptom to its fix. Each entry names the likely cause and the one change that resolves it, then points you to the page with the full detail. For the meaning of a specific status code, the [Errors](/errors) reference is the canonical table; for short answers to common questions, start with the [FAQ](/faq).

## Install and data flow

<AccordionGroup>
  <Accordion title="The snippet does not load, or no data appears" icon="plug-circle-xmark">
    **Symptom.** Nothing reaches your analytics dashboard or webhook, and the browser never posts an identification.

    **Cause.** One of three things is blocking the snippet before it can run:

    * A **Content Security Policy** is refusing the hosts the snippet needs. The module and its dependency load under `script-src`, and the identification and network checks post under `connect-src`. A policy missing those hosts stops the snippet cold.
    * An **ad blocker or content blocker** is dropping the CDN host, so the module never downloads.
    * The page is **not served over HTTPS**. The snippet collects signals in a secure context only, so it does not run on plain `http://` or a non-secure origin.

    **Fix.** Open the browser dev tools. A CSP block shows a `Refused to load` or `Refused to connect` error naming the directive and host, which is your signal to add that host. The exact directives and host list live on the [CSP setup](/setup/csp) page, and the [snippet install guide](/setup/snippet) shows the HTML and framework methods. Confirm the page loads over HTTPS, then load it with any blockers disabled to rule the extension in or out. Make sure the page also calls `checkAnonymous()` or `checkAuthenticatedUser(hashedId)` once the module loads. Once identifications arrive, **Integration > Domains** in the analytics dashboard shows the domain as **Reporting**.
  </Accordion>

  <Accordion title="No webhook arrives" icon="bell-slash">
    **Symptom.** The identification runs and counts, its Risk Score shows in the analytics dashboard, but your endpoint never receives the POST.

    **Cause.** Either no webhook endpoint is active for the domain, or the delivery was dropped. Webhook delivery is at-most-once, with no retries and a 1-second timeout, so a slow, down, or non-2xx endpoint silently loses that delivery, and there is no resend.

    **Fix.** In the [analytics dashboard](https://app.shieldlabs.ai/), open **Integration > Webhooks** and check the domain's endpoints: at least one must be switched on, not **Paused**. Use **Test** on the endpoint to confirm it answers `2xx`; the result shows the HTTP status and how long the delivery took, as the [webhook setup](/setup/webhooks) covers. Make your handler return `200` fast, then do slow work asynchronously so you stay inside the timeout. Because a single delivery can always be lost, treat the [History API](/api/server-api) as the guaranteed read: look the result up by `request_id` whenever it must not be missed.

    <Frame caption="Integration > Webhooks in the analytics dashboard: each endpoint has its own signing secret, Verify and Test.">
      <img className="block dark:hidden" src="https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/integration-webhooks.png?fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=08f2e19d5b5f630ee6e9f57665adab46" alt="Integration > Webhooks for example.com in the analytics dashboard: 2 endpoints (limit 10), each Delivering with a masked whsec_ signing secret and its last delivery 2m ago; row action icons Pause, Edit, Test, Verify, Rotate secret and Delete; the Test result Delivered HTTP 200 in 184 ms; and the Verify a signature sample for Node.js." data-og-width="2270" width="2270" data-og-height="2224" height="2224" data-path="images/dashboard/integration-webhooks.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/integration-webhooks.png?w=280&fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=3e7be1f69a30fed36f0c820b10cea09c 280w, https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/integration-webhooks.png?w=560&fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=e6c4e8bd6c4cc1fa9088922f1ea89ce4 560w, https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/integration-webhooks.png?w=840&fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=6d612b21da68a4a799945c7fe775519a 840w, https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/integration-webhooks.png?w=1100&fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=c226268f4601c485a03c40b3ead0067a 1100w, https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/integration-webhooks.png?w=1650&fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=5add9ec06cceb637685c47e8a356869a 1650w, https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/integration-webhooks.png?w=2500&fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=9f0966cd14ae16075f288e8759c78f80 2500w" />

      <img className="hidden dark:block" src="https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/integration-webhooks-dark.png?fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=bf1899621c76b377d674e908a72cb688" alt="Integration > Webhooks for example.com in the analytics dashboard in the dark theme: 2 endpoints (limit 10), each Delivering with a masked whsec_ signing secret and its last delivery 2m ago; row action icons Pause, Edit, Test, Verify, Rotate secret and Delete; the Test result Delivered HTTP 200 in 184 ms; and the Verify a signature sample for Node.js." data-og-width="2270" width="2270" data-og-height="2224" height="2224" data-path="images/dashboard/integration-webhooks-dark.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/integration-webhooks-dark.png?w=280&fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=5eb12ec85dc02daa4956e46ae56cb18a 280w, https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/integration-webhooks-dark.png?w=560&fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=2eb2c93cdb0ef45c58723c6b1fbd0177 560w, https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/integration-webhooks-dark.png?w=840&fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=66591544a4bee4e5d1721e494546877d 840w, https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/integration-webhooks-dark.png?w=1100&fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=494d3573096df77554f7ed54126d55a7 1100w, https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/integration-webhooks-dark.png?w=1650&fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=6fbb8001bc4a5d29368a664d88a9713f 1650w, https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/integration-webhooks-dark.png?w=2500&fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=46dd0811ae2b9ce48f61a1d04b6afe79 2500w" />
    </Frame>
  </Accordion>

  <Accordion title="Signature verification fails on a valid webhook" icon="key-skeleton">
    **Symptom.** The payload looks correct, but your HMAC check rejects it.

    **Cause.** You are hashing a re-serialized copy of the JSON, or using the domain Secret Key instead of the endpoint's `whsec_…` secret. Parsing the body and re-encoding changes the bytes, so the HMAC no longer matches.

    **Fix.** Compute HMAC-SHA256 over the **raw request body bytes exactly as received**, keyed with that endpoint's `whsec_…` signing secret, prefix with `sha256=`, and constant-time compare against `X-Shield-Signature`. The [webhook setup](/setup/webhooks) page has working Node, Go, and Python examples that do this correctly.
  </Accordion>
</AccordionGroup>

## Reading the result

<AccordionGroup>
  <Accordion title="The Device ID comes back all zeros" icon="fingerprint">
    **Symptom.** An identification carries a `device_id` of `00000000-0000-0000-0000-000000000000`.

    **Cause.** An all-zero Device ID means no usable device signals reached ShieldLabs for that identification; the rate-limit marker (Risk Score `999`) is one such case.

    **Fix.** Route it to review rather than allowing it. Read its risk signals and the account's other identifications by `user_hid` through the [History API](/api/server-api#read-every-identification-of-one-account). A `999` row is the gateway's rate-limit marker, so keep it out of Risk Score logic (see the `429` entry below). The [Identifiers](/features/identification) page covers the all-zero case.
  </Accordion>

  <Accordion title="A legitimate user scores high" icon="user-check">
    **Symptom.** A real customer lands in the Suspicious or Dangerous band with no wrongdoing.

    **Cause.** A corporate VPN, a proxy, or a privacy-focused browser raises the Risk Score on its own. The signals are real, but they describe the connection, not the person's intent.

    **Fix.** 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 signals show why, and you choose the action for each case. Check the account too: its other identifications (History API by `user_hid`), its linked devices and IPs, and whether a High-Risk Event is on it, in the analytics dashboard, the API or webhooks. A withdrawal warrants a stricter line than a page view. The guide to [acting on results](/guides/acting-on-risk-score) walks through the three bands (Trusted 0-29, Suspicious 30-59, Dangerous 60-100) and adjusting your cut-offs gradually so legitimate VPN users keep access.
  </Accordion>

  <Accordion title="Reading a user's, device's or IP's first and last activity" icon="clock-rotate-left">
    **Symptom.** You want the history of an account, device, visitor or IP, or its earliest and latest sighting.

    **Cause.** The [History API](/api/server-api) returns identifications, newest first, each with its `created_at` time. First and last activity come from the two ends of that list.

    **Fix.** Read the History API by `user_hid`, `device_id`, `visitor_id` or `ip`. The first row is the most recent activity; page with `limit` (1 to 100) and `offset` to the last row for the earliest one stored. Reads never count toward your plan. In the analytics dashboard, every user, device, visitor and public IP [card](/dashboard/entity-card) shows **First seen** and **Last seen** for the selected period.
  </Accordion>

  <Accordion title="No User HID or High-Risk Events appear" icon="user-slash">
    **Symptom.** Identifications arrive, but every one carries a `user_hid` of `"anonymous"`, and your users carry no High-Risk Events.

    **Cause.** Your pages call `checkAnonymous` only, so no identification carries a User HID. Users, account-level risk and all four High-Risk Events are built on the User HID.

    **Fix.** Pass a hashed User HID with `checkAuthenticatedUser` on every signed-in page, and call `forceCheckAuthenticatedUser(hashedUserId)` right after login, with a hashed or pseudonymous account id. The [snippet](/setup/snippet#identify-signed-in-users) page shows the signed-in call. An event also needs its evidence: by default, Multi-accounting fires from 3 accounts on one visitor (one device plus one cookie) and Account sharing from 4 devices on one account, and both thresholds are configurable.

    To check one call, open its [identification card](/dashboard/identification-card): a note under **Details** says when the call passed no User HID and so has no user to link, and **Risk of the identities in this call** shows no User risk.

    <Frame caption="An identification sent without a User HID has no user to link, in the analytics dashboard.">
      <img className="block dark:hidden" src="https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/identification-no-user-hid.png?fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=a63256f59adbde2e4418483001b340e1" alt="An identification in the analytics dashboard sent without a User HID: the User HID field shows a dash, a note under Details says the call passed no hashed account id and so has no user to link, and Risk of the identities in this call shows only Visitor risk, Device risk and IP risk." width="2238" height="788" data-path="images/dashboard/identification-no-user-hid.png" />

      <img className="hidden dark:block" src="https://mintcdn.com/shieldlabs-725d18f1/JyleDzUFYU3SXP4Q/images/dashboard/identification-no-user-hid-dark.png?fit=max&auto=format&n=JyleDzUFYU3SXP4Q&q=85&s=8e781df0b82a709b2bf8d2a173ab51d0" alt="An identification in the analytics dashboard in the dark theme sent without a User HID: the User HID field shows a dash, a note under Details says the call passed no hashed account id and so has no user to link, and Risk of the identities in this call shows only Visitor risk, Device risk and IP risk." width="2238" height="788" data-path="images/dashboard/identification-no-user-hid-dark.png" />
    </Frame>
  </Accordion>
</AccordionGroup>

## Status codes and limits

<AccordionGroup>
  <Accordion title="HTTP 402 on the identification request" icon="wallet">
    **Symptom.** The snippet's identification request returns `402`.

    **Cause.** Your account has used its included volume. Only the identification request returns `402`; History API and profile reads never do.

    **Fix.** Identification resumes when the billing cycle resets or when you change plan, as the [Billing](/billing) page lays out. A `402` is a billing state and is unrelated to rate limiting.
  </Accordion>

  <Accordion title="HTTP 429, or a Risk Score of 999" icon="gauge-high">
    **Symptom.** The gateway returns `429`, or a webhook or History row arrives with a Risk Score of `999` (`data.risk_score` on the webhook, `score` on the History API).

    **Cause.** A `429` is a gateway protection, separate from the Risk Score. It can come from the per-IP limit (15/min, then a 10-minute ban), the per-domain ingest budget, or a **domain freeze** after 10 seconds at the plan cap. Only the **per-IP ban** can also surface a `999` marker on a webhook delivery or a History row. A domain freeze uses the same `429` body, is not billed, and does **not** emit `999`.

    **Fix.** After verifying `X-Shield-Signature`, check for the marker before your decision logic: treat any value above 100 as the rate-limit marker and route the action it belongs to to review.

    ```js theme={null}
    // After verifying X-Shield-Signature on the raw body and acknowledging with 200.
    // 999 is the rate-limit ban marker, not a Risk Score: keep it out of your
    // Risk Score logic and send the action it belongs to for review.
    const data = req.body.data;
    if (data.risk_score > 100) return routeToReview(data.request_id); // History API rows: row.score
    ```

    The Risk Score is 0 to 100; the only exception is the `999` marker, which means "this IP was banned at the gateway." Soft domain `429`s and a domain freeze do not write `999`. The limits are on the [rate limits](/rate-limits) page.
  </Accordion>

  <Accordion title="Checking whether the service is up" icon="heart-pulse">
    **Symptom.** You want a liveness probe for monitoring or a load balancer health check.

    **Cause.** You need a lightweight endpoint that confirms a gateway is serving, without counting toward your plan or running a scoring path.

    **Fix.** Each gateway exposes a `GET /health` endpoint that returns `200` with `{ "status": "ok" }`. Point your uptime monitor or orchestrator liveness probe at it. It needs no key and never counts toward your plan.
  </Accordion>
</AccordionGroup>

## Still stuck

If a status code is the question, the [Errors](/errors) page is the full per-surface reference, and the [FAQ](/faq) answers the questions developers ask most about keys, identifications, identifiers and a Risk Score of 0. [Support](/support) is there by chat and email on every plan.


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