> ## 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 dashboard or webhook, and the browser never posts a snapshot.

    **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 snapshot 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. Loading the module is not enough on its own: you must also call `checkAnonymous()` or `checkAuthenticatedUser(hashedId)`.
  </Accordion>

  <Accordion title="No webhook arrives" icon="bell-slash">
    **Symptom.** The check runs and bills, the score lands in the dashboard, but your endpoint never receives the POST.

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

    **Fix.** Confirm at least one **enabled** endpoint is registered on the domain's **Webhooks** tab in the [dashboard](https://app.shieldlabs.ai/), 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.
  </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 DeviceID comes back all zeros" icon="fingerprint">
    **Symptom.** A result carries `DeviceID` of `00000000-0000-0000-0000-000000000000`, and that visit scores 90.

    **Cause.** No stable device characteristics reached the server, so no identity could be built. This happens when the snippet was blocked or JavaScript was disabled. The visit still scores, and a visit with nothing to identify scores 90.

    **Fix.** Route a null or all-zero DeviceID to review rather than auto-allowing it. You cannot recognize a returning person from an identity that was never collected, so treat the absence as a signal in its own right. The handling and the all-zero case are described on the [Identification](/features/identification) page.
  </Accordion>

  <Accordion title="A legitimate user scores high" icon="user-check">
    **Symptom.** A real customer lands in the Medium or High 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.** Decide on the **Score plus its `signals` plus the action context**, never the number alone. A withdrawal warrants a stricter line than a page view, and the `signals` tell you which signals fired so you can weigh them. Working from the four bands, the guide to [acting on the Risk Score](/guides/acting-on-risk-score) walks through tuning thresholds gradually so legitimate VPN users are not punished.
  </Accordion>

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

    **Cause.** There is no dedicated first-seen field on a result. History is a list of point-in-time snapshots, and each one carries `LastRequestTime`, the moment that snapshot was recorded.

    **Fix.** Read the [History API](/api/server-api) by `device_id`, `visitor_id`, `user_hid`, or `ip`. Rows come back newest first, so the first row is the most recent activity and the last row in a full result is the earliest you have stored. The `LastRequestTime` on any row is when that visit was seen. Each returned row bills 1 request, so set `limit` to the smallest value that answers your question.
  </Accordion>
</AccordionGroup>

## Status codes and limits

<AccordionGroup>
  <Accordion title="HTTP 402 on ingest or the Server API" icon="wallet">
    **Symptom.** The snippet's snapshot post, or a Server API call, returns `402`.

    **Cause.** Your request balance is exhausted. A `402` appears in two places when you run out: when the snippet posts an identification to be scored, and on Management API History calls. The Management History path bills one request per returned row, so a wide search can drain a low balance quickly. Account History reads on `account.shieldlabs.ai` are free.

    **Fix.** Top up your request balance or upgrade your 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 Score of 999" icon="gauge-high">
    **Symptom.** The gateway returns `429`, or a webhook or History row arrives with `Score` of `999`.

    **Cause.** Both come from the per-IP rate limit, a gateway protection rather than a verdict. The browser receives the `429`, and a rate-limit-banned request can also surface a `999` marker on a webhook delivery or a History row. That `999` is not capped to 100, so it arrives as-is.

    **Fix.** Guard for it at the very top of your handler so it never reaches your decision logic:

    ```js theme={null}
    // 999 is the rate-limit ban marker, never a customer score.
    if (score > 100) return; // webhook: score; History: Score
    ```

    The Risk Score is always 0 to 100. Read `999` as "this IP was rate-limited," and spread traffic across client IPs rather than proxying through one server. The thresholds and the ban window 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 spending a request 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 does not authenticate and does not bill.
  </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, requests, identifiers, and a score of 0.
