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

# Data Models

> A reference for every field ShieldLabs returns and what each one means.

This page is the schema reference for every object the ShieldLabs API and webhooks return. Four objects carry the result of an identification:

* **`WebhookEvent`** is the signed JSON envelope pushed to each configured endpoint (snake\_case): `event_type`, `schema_version`, `created_at`, and scored fields under `data`.
* **`Snapshot`** is the richer object the **Management** History API (`api.shieldlabs.ai`) returns (PascalCase). It is a superset of the webhook visit fields with network fields added. The recommended History API on `account.shieldlabs.ai` returns the same data as **snake\_case** rows in a `{ data, total }` envelope — see [Server API](/api/server-api).
* **`ScoreDetail`** is one entry in the explainable `Details` array on a **Snapshot** (`Value`, `Description`).
* **`WebhookSignal`** is one entry in the explainable `signals` array inside `WebhookEvent.data` (`name`, `weight`).

A fifth object, **`Profile`**, is the per-domain configuration you read from the Server API.

<Note>
  ShieldLabs surfaces these objects. Your own code owns the decision. The payloads carry a [Risk Score](/features/risk-scoring) (0–100) and the [signals](/features/anonymity-signals) behind it. You decide allow / challenge / review / block in your application from that data.
</Note>

## Object map

<CardGroup cols={2}>
  <Card title="WebhookEvent" icon="bolt">
    The signed webhook envelope: `event_type`, `schema_version`, `created_at`, and scored `data`.
  </Card>

  <Card title="Snapshot" icon="server">
    Returned by the Management History API (PascalCase). Everything in the webhook payload plus connection and network fields.
  </Card>

  <Card title="WebhookSignal" icon="list-check">
    One signal in a webhook `data.signals` entry: `{ name, weight }`.
  </Card>

  <Card title="ScoreDetail" icon="list-check">
    One signal in a Snapshot `Details` entry: `{ Value, Description }`.
  </Card>

  <Card title="Profile" icon="gear">
    Your domain's configuration: weight balance and masked keys.
  </Card>
</CardGroup>

## WebhookEvent

The envelope delivered when ShieldLabs finishes scoring an identification. Each enabled endpoint receives **one POST per check**. Field names are **snake\_case**. The HMAC signature travels in the `X-Shield-Signature` header, not in the body. See [Webhooks](/api/webhooks) for verification, service event types, and delivery timing.

```json theme={null}
{
  "event_type": "identification.scored",
  "schema_version": "2026-06-01",
  "created_at": "2026-06-26T14:20:42Z",
  "data": {
    "request_id": "13f84f05-7c2a-4e9b-9f1d-2a6b8c0e4d11",
    "visitor_id": "161dfbad-8e7f-4a6b-9c5d-0e1f2a3b4c5d",
    "device_id": "5eb7fd5c-2a1b-4c3d-9e8f-7a6b5c4d3e2f",
    "session_id": "7a1b2c3d-4e5f-6789-abcd-ef0123456789",
    "cookie_id": "3f2e1d0c-9b8a-7654-3210-fedcba987654",
    "user_hid": null,
    "domain": "example.com",
    "public_ip": { "ip": "203.0.113.42", "country": "US" },
    "local_ip": { "ip": "198.51.100.23", "country": "DE" },
    "connection_type": "proxy",
    "os": "Windows",
    "browser": "Chrome",
    "device_type": "desktop",
    "traffic_source": {
      "channel": "Google Ads",
      "referrer_domain": "google.com",
      "landing_url": "https://example.com/lp?gclid=abc123",
      "click_id_type": "gclid",
      "utm_source": "google",
      "utm_medium": "cpc",
      "utm_campaign": "summer_sale",
      "utm_content": "ad_a",
      "utm_term": "buy shoes"
    },
    "risk_score": 30,
    "signals": [
      { "name": "proxy", "weight": 10 },
      { "name": "datacenter_ip", "weight": 10 },
      { "name": "abuser", "weight": 10 }
    ],
    "detection_flags": {
      "vpn": false,
      "privacy_relay": false,
      "browser_vpn_proxy": false,
      "tor": false,
      "proxy": true,
      "datacenter_ip": true,
      "abuser": true,
      "os_mismatch": false,
      "os_not_detected": false,
      "timezone_mismatch": false,
      "anti_detect_browser": false,
      "javascript_disabled": false,
      "incognito": false,
      "suspicious_paid_click": false,
      "stun_not_checked": false,
      "ip_mismatch": true
    },
    "observed_at": "2026-06-26T14:20:42Z"
  }
}
```

<ResponseField name="event_type" type="string">
  `identification.scored` for real visits; `webhook.ping` for endpoint Verify. See [Webhooks](/api/webhooks#service-events).
</ResponseField>

<ResponseField name="schema_version" type="string">
  Contract version, currently `2026-06-01`.
</ResponseField>

<ResponseField name="created_at" type="string (RFC 3339)">
  When the webhook event was created.
</ResponseField>

<ResponseField name="data" type="object">
  Scored visit payload. Omitted on `webhook.ping`. Fields below describe `data` on `identification.scored`.
</ResponseField>

<ResponseField name="request_id" type="string (UUID)">
  The client-generated UUID for this identify call. Use as the idempotency key and the join key to the [History API](/api/server-api).
</ResponseField>

<ResponseField name="session_id" type="string (UUID)">
  The per-visit identifier from `sessionStorage` (short-lived). Resets each browser session or tab.
</ResponseField>

<ResponseField name="cookie_id" type="string (UUID)">
  The first-party cookie / localStorage identifier minted in the browser. It is lost when the visitor clears cookies or storage.
</ResponseField>

<ResponseField name="device_id" type="string (UUID)">
  Server-derived from hundreds of stable browser and device characteristics. It **survives cookie clears, incognito, and IP rotation** because it is derived from the environment, not stored, as the [Identifiers](/features/identification) reference explains.
</ResponseField>

<ResponseField name="visitor_id" type="string (UUID)">
  Server-derived from `device_id` plus `cookie_id`. It **changes when the cookie is cleared**. Multiple `visitor_id` values can map to one `device_id`. The durability claim belongs to `device_id`, not `visitor_id`.
</ResponseField>

<ResponseField name="user_hid" type="string | null">
  Your own account identifier, passed in through the snippet. **Always pass a hashed or pseudonymous value, never a raw email or user id.** `null` when not provided; literal `anonymous` on anonymous calls.
</ResponseField>

<ResponseField name="domain" type="string">
  The registered site domain key for this identification.
</ResponseField>

<ResponseField name="public_ip" type="object">
  Public HTTP IP and country: `{ "ip": "<dotted IPv4>", "country": "<ISO code>" }`.
</ResponseField>

<ResponseField name="local_ip" type="object">
  The real network IP and country from an optional follow-up network check, when captured. Same shape as `public_ip`. Both `ip` and `country` may be empty when no local IP was resolved.
</ResponseField>

<ResponseField name="os" type="string">
  The operating system derived for the visitor, for example `Windows`, `Mac OS X`, or `IOS (iPhone)`. May be empty when the OS could not be determined.
</ResponseField>

<ResponseField name="browser" type="string">
  Browser family, for example `Chrome`.
</ResponseField>

<ResponseField name="device_type" type="string">
  `desktop`, `mobile`, or `tablet`.
</ResponseField>

<ResponseField name="connection_type" type="string">
  The classified connection type, one of `direct`, `mobile`, `vpn`, `proxy`, `tor`, `privacy_relay`, `browser_vpn_proxy`, or `unknown` (when the type could not be resolved).
</ResponseField>

<ResponseField name="risk_score" type="integer (0–100)">
  The explainable [Risk Score](/features/risk-scoring). Integer, hard-capped at 100. Higher means more anonymous, masked, spoofed, or abusive. Map bands in your code: Clean 0–9, Low 10–29, Medium 30–59, High 60–100. The value 999 is a rate-limit ban marker; guard `risk_score > 100` before reading the band.
</ResponseField>

<ResponseField name="signals" type="WebhookSignal[]">
  The **full** list of scored signals that contributed to `risk_score`. Each entry follows the [WebhookSignal](#webhooksignal) shape below.
</ResponseField>

<ResponseField name="traffic_source" type="object">
  Traffic attribution: `channel`, `referrer_domain`, `landing_url`, `click_id_type`, and five UTM fields when present.
</ResponseField>

<ResponseField name="detection_flags" type="object">
  Boolean detection flags: `vpn`, `privacy_relay`, `browser_vpn_proxy`, `tor`, `proxy`, `datacenter_ip`, `abuser`, `os_mismatch`, `os_not_detected`, `timezone_mismatch`, `stun_not_checked`, `anti_detect_browser`, `javascript_disabled`, `incognito`, `suspicious_paid_click`, `ip_mismatch`.
</ResponseField>

<Note>
  Branch on `risk_score`, signal `name` slugs, and `detection_flags`.
</Note>

<ResponseField name="observed_at" type="string (RFC 3339)">
  When the visit was scored and this payload was finalized, for example `2026-06-26T14:20:42Z`.
</ResponseField>

## WebhookSignal

One entry in the webhook `data.signals` array. Each entry is a signal that fired and the points it contributed.

```json theme={null}
{ "name": "anti_detect_browser", "weight": 30 }
```

<ResponseField name="name" type="string">
  Stable machine slug, for example `vpn`, `datacenter_ip`, or `os_mismatch`. Match against the [signal reference](/features/anonymity-signals). Safe to branch on in application code.
</ResponseField>

<ResponseField name="weight" type="integer">
  The points this signal added to the Risk Score. `weight` can be **signed** (negative when a follow-up check lowers the score). Always read `risk_score` for the running total; never reconstruct it by summing `signals`.
</ResponseField>

## ScoreDetail

One entry in the **Snapshot** `Details` array (History API, PascalCase). It is what makes the Risk Score explainable on stored snapshots.

```json theme={null}
{ "Value": 30, "Description": "Anti-detect Browser" }
```

<ResponseField name="Value" type="integer">
  The points this signal added to the Risk Score. The score is additive: the entries' `Value` fields sum to the total, then the total is capped at 100. Use the weight table below to interpret a `Value`.
</ResponseField>

<ResponseField name="Description" type="string">
  The customer-facing name of the signal, for example `"VPN"`, `"OS Mismatch"`, or `"Datacenter IP"`. Match this against the [signal reference](/features/anonymity-signals) to understand what fired.
</ResponseField>

### Interpreting `Value`: the signal-weight reference

Each signal contributes a fixed number of points to the Risk Score, and a higher weight is stronger evidence of anonymity or spoofing. The full weight table lives on [Risk Scoring](/features/risk-scoring); match a `Description` against the [signal reference](/features/anonymity-signals) to see what each one covers.

<Tip>
  A high `Value` does not by itself mean fraud. A real user behind a corporate proxy, a VPN, or a privacy browser can legitimately fire these checks. Decide on the **Score plus the Details plus your own context**, never the raw number alone, following the [per-band playbook](/guides/acting-on-risk-score).
</Tip>

## Snapshot

The object returned by the **Management** History API (`GET /v1/history/{type}/{value}` on `api.shieldlabs.ai`). A `Snapshot` is a **superset of the webhook payload**: it carries the same identity, score, traffic, and detection fields in **PascalCase** and adds raw network columns. That endpoint returns an array of these, newest first. (The recommended History API on `account.shieldlabs.ai` returns the same data as snake\_case rows in a `{ data, total }` envelope.)

```json theme={null}
[
  {
    "RequestID":            "550e8400-e29b-41d4-a716-446655440000",
    "SessionID":            "7a1b2c3d-4e5f-6789-abcd-ef0123456789",
    "CookieID":             "3f2e1d0c-9b8a-7654-3210-fedcba987654",
    "DeviceID":             "d290f1ee-6c54-4b01-90e6-d701748f0851",
    "VisitorID":            "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "IP":                   "203.0.113.10",
    "ConnectionType":       "vpn",
    "OS":                   "Windows",
    "Browser":              "Chrome",
    "DeviceType":           "desktop",
    "Country":              "US",
    "UserHID":              "e3b0c44298fc1c149afbf4c8996fb924",
    "Score":                15,
    "Details":              [{ "Value": 15, "Description": "VPN" }],
    "LastRequestTime":      "2026-06-16T10:00:00Z"
  }
]
```

The identity, score, and `Details` fields map to webhook fields as follows: `RequestID` ↔ `request_id`, `Score` ↔ `risk_score`, `Details[{ Value, Description }]` ↔ `signals[{ name, weight }]`, `IP`/`Country` ↔ `public_ip`, the network-check IP fields ↔ `local_ip`, traffic columns ↔ `traffic_source`, signal flags ↔ `detection_flags`. The fields a `Snapshot` adds on top:

<ResponseField name="ConnectionType" type="string">
  The classified connection type, one of `direct`, `mobile`, `vpn`, `proxy`, `tor`, `privacy_relay`, `browser_vpn_proxy`, or `unknown` (when the type could not be resolved).
</ResponseField>

<ResponseField name="Browser" type="string">
  The browser derived for the visitor, for example `Chrome` or `Safari`.
</ResponseField>

<ResponseField name="DeviceType" type="string">
  The device form factor, for example `desktop` or `mobile`.
</ResponseField>

The snapshot may include additional network-intelligence fields.

<Warning>
  The additional network-intelligence fields are raw network internals. They power the score; they are not meant for end-user display. Keep them on your server.
</Warning>

The [History API](/api/server-api) on `account.shieldlabs.ai` accepts `request_id`, `visitor_id`, `device_id`, `user_hid`, `ip`, `session_id`, and `cookie_id`. The alternate Management History path on `api.shieldlabs.ai` accepts the first five only and bills 1 request per returned row. The full query, `limit` rules, and response shapes are in the [Server API](/api/server-api).

## Profile

The per-domain configuration object returned by `GET /v1/profile` on `api.shieldlabs.ai`. This read is free (it does not consume requests).

```json theme={null}
{
  "Domain":    "yourapp.com",
  "Weight":    142850,
  "PublicKey": "••••••••-••••-••••-••••-••••••••a1b2",
  "Secret":    "••••••••••••••••••••3f9c",
  "CreatedAt": "2026-01-04T09:30:00Z"
}
```

<ResponseField name="Domain" type="string">
  The domain this configuration belongs to.
</ResponseField>

<ResponseField name="Weight" type="integer">
  Your remaining request balance for this domain. Each identification consumes 1; each History row consumes 1, as the [Billing](/billing) page details.
</ResponseField>

<ResponseField name="PublicKey" type="string (masked)">
  Your per-domain [Public Key](/setup/keys), masked to the last four characters. The Public Key goes in the snippet URL and is safe to expose. Use the [dashboard](https://app.shieldlabs.ai/) to view it in full.
</ResponseField>

<ResponseField name="Secret" type="string (masked)">
  Your per-domain [Secret Key](/setup/keys), masked to the last four characters. The Secret Key is backend-only: it authenticates the Management API (`api.shieldlabs.ai`). It does **not** sign webhooks; each webhook endpoint has its own `whsec_…` secret. Never put the Secret Key in the browser.
</ResponseField>

<ResponseField name="CreatedAt" type="string (RFC 3339)">
  When the domain configuration was created.
</ResponseField>

## Patterns are not in these payloads

The [8 Patterns](/features/patterns) are a **dashboard feature**, not part of the API or webhook payloads above. They are computed server-side from your historical data, graded over a time window, and shown on the dashboard Patterns view. You retrieve them through dashboard export (CSV or JSON), not from the webhook body, `Snapshot`, or any API field documented on this page.

They are also conceptually different from the scoring [signals](/features/anonymity-signals):

|          | Signals (in these payloads)                               | Patterns (dashboard export only)  |
| -------- | --------------------------------------------------------- | --------------------------------- |
| Scope    | One request, in real time                                 | One identity, over a time window  |
| Output   | Webhook `signals` / Snapshot `Details` plus score (0–100) | A graded detection per entity     |
| Delivery | Webhook and History API                                   | Dashboard view and export         |
| Grading  | Points that sum into the Risk Score                       | `suspicious` or `dangerous` level |

For completeness, the shape an exported pattern detection follows conceptually:

<ResponseField name="pattern" type="string">
  The pattern's machine name, for example `many_accounts_one_device` or `multiple_countries_on_account`. All 8 [Patterns](/features/patterns) are named there with what each detects.
</ResponseField>

<ResponseField name="entity_type" type="string">
  The kind of identifier flagged: `user_hid`, `visitor_id`, `device_id`, `cookie_id`, or the Local IP entity (a dashboard label).
</ResponseField>

<ResponseField name="entity_id" type="string">
  The specific identifier value that was flagged.
</ResponseField>

<ResponseField name="level" type="string">
  The grade: `suspicious` or `dangerous`. An entity that never crosses the suspicious threshold is implicitly "Normal" and is not recorded. A level never downgrades on later runs.
</ResponseField>

<Note>
  Pattern thresholds are server-side and adapt to an entity's risk. There is no in-product rules engine: ShieldLabs grades patterns and scores requests, and your own code (using the Risk Score and signals from webhooks and the API) owns the allow / challenge / review / block decision.
</Note>

## Related

<CardGroup cols={2}>
  <Card title="Webhooks" icon="bolt" href="/api/webhooks">
    The signed webhook envelope, `X-Shield-Signature` verification, and at-most-once delivery.
  </Card>

  <Card title="Server API" icon="server" href="/api/server-api">
    History search, profile, `limit` rules, and per-row billing.
  </Card>

  <Card title="Risk Score" icon="gauge" href="/features/risk-scoring">
    How the 0–100 explainable score and the Clean / Low / Medium / High bands work.
  </Card>

  <Card title="Signals" icon="list-check" href="/features/anonymity-signals">
    The full catalog of scoring signals, their weights, and how they combine.
  </Card>
</CardGroup>
