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

> Every object the webhook and the Server API return, and the user, device, visitor or IP each identifier belongs to.

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 PascalCase object the **deprecated** Management History path (`GET /v1/history/…` on `api.shieldlabs.ai`) still returns. It carries the identity and score fields in PascalCase plus network columns, without the webhook's traffic source or detection flags. The **History API** on `account.shieldlabs.ai` is the recommended read: **snake\_case** rows in a `{ data, total }` envelope. See [Server API](/api/server-api).
* **`ScoreDetail`** is one `{ Value, Description }` entry, in the deprecated Snapshot `Details` array and in the parsed History `score_details` string.
* **`WebhookSignal`** is one entry in the explainable `signals` array inside `WebhookEvent.data` (`name`, `weight`).

A fifth object, **`Profile`**, is what the Management API returns: the domain, the remaining included volume on your account and masked keys.

<Note>
  Each payload is one identification: a [Risk Score](/features/risk-scoring) (0-100) with the [risk signals](/features/risk-signals) behind it, and the user, device, visitor and IPs it belongs to. You choose the action for each case (allow, step up, review or block) and act on the result in your backend.
</Note>

## Object map

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

  <Card title="Snapshot" icon="server">
    Returned by the deprecated Management History path (PascalCase). Prefer History API snake\_case rows.
  </Card>

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

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

  <Card title="Profile" icon="gear">
    Your domain, the remaining included volume on your account and masked keys.
  </Card>
</CardGroup>

## Identifiers and the identities they map to

An identification is the event layer: one check by the snippet, with one Risk Score. Five of its fields are identities that ShieldLabs scores and links over time; the others identify the call itself.

| Field in `data` | Identity | History `search_type` | What it is |
| - | - | - | - |
| `user_hid` | User (your account) | `user_hid` | The hashed User HID you pass with `checkAuthenticatedUser`; `anonymous` on anonymous checks. Users, account-level risk and all four High-Risk Events are built on it. |
| `device_id` | Device | `device_id` | The durable device. Holds through cleared cookies, incognito mode and IP changes; another browser gets its own Device ID. |
| `visitor_id` | Visitor | `visitor_id` | One device plus one cookie. Changes when cookies are cleared. |
| `public_ip` | Public IP | `ip` | The public address, with its country. |
| `local_ip` | Local IP | none | The address the browser itself reports, which can differ from the public IP behind a VPN or proxy. Keep it from the webhook to group by it. |
| `request_id` | The identification | `request_id` | One call. The idempotency and join key. |
| `session_id`, `cookie_id` | Context of the call | `session_id`, `cookie_id` | The browsing session and the first-party cookie. |

Each user, device, visitor and IP takes the worst band of its identifications; [Read every identification of one account](/api/server-api#read-every-identification-of-one-account) shows how to compute it. High-Risk Events are detected on users, each at Medium or High confidence; see [High-Risk Events](#high-risk-events) below. In the analytics dashboard they show on [Overview](/dashboard/overview) and on each [user card](/dashboard/entity-card). [Users, devices, visitors and IPs](/concepts/entities) explains the model.

## WebhookEvent

The envelope delivered when ShieldLabs finishes scoring an identification. Each enabled endpoint receives **one POST per identification**. 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": "anonymous",
    "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": 40,
    "signals": [
      { "name": "proxy", "weight": 10 },
      { "name": "datacenter_ip", "weight": 10 },
      { "name": "abuser", "weight": 10 },
      { "name": "timezone_mismatch", "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": true,
      "anti_detect_browser": false,
      "browser_automation": false,
      "javascript_disabled": false,
      "incognito": false,
      "search_bot": false,
      "suspicious_paid_click": false,
      "stun_not_checked": false,
      "ip_mismatch": true,
      "check_incomplete": false
    },
    "observed_at": "2026-06-26T14:20:42Z"
  }
}
```

<ResponseField name="event_type" type="string">
  `identification.scored` for every scored identification; `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">
  The scored identification. Omitted on `webhook.ping`. Fields below describe `data` on `identification.scored`.
</ResponseField>

<ResponseField name="request_id" type="string (UUID)">
  The client-generated UUID of this identification. 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 Session ID: the browsing session, written by the snippet to `localStorage` (legacy `sessionStorage` keys are read once and migrated), so it is shared across tabs rather than tied to one tab.
</ResponseField>

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

<ResponseField name="device_id" type="string (UUID)">
  The Device ID, server-derived and not stored in the browser. It **holds through cleared cookies, incognito mode and IP changes**; another browser on the same machine gets its own Device ID. The [Identifiers](/features/identification) reference explains it.
</ResponseField>

<ResponseField name="visitor_id" type="string (UUID)">
  The Visitor ID, 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">
  The User HID: your account key, passed with `checkAuthenticatedUser`. **Always pass a hashed or pseudonymous value, never a raw email or user id.** Literal `anonymous` on anonymous checks; `null` when an empty string is passed and on the test delivery sample. Users, account-level risk and all four High-Risk Events are built on it.
</ResponseField>

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

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

<ResponseField name="local_ip" type="object">
  The local IP: the address the browser itself reports, with its 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 device, 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">
  The explainable [Risk Score](/features/risk-scoring) of this identification, an integer from 0 to 100. Higher means riskier: more likely masked, spoofed, or abusive. The payload carries the number, with no band field. Map it to a band in your backend: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. The only value above 100 is 999, the rate-limit marker; guard `risk_score > 100` before reading the band.
</ResponseField>

<ResponseField name="signals" type="WebhookSignal[]">
  The **full** list of risk signals with a non-zero weight. A slug can repeat with a partial weight when an earlier verdict is carried forward; read `risk_score` for the total. 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`, `browser_automation`, `javascript_disabled`, `incognito`, `search_bot`, `suspicious_paid_click`, `ip_mismatch`, `check_incomplete`.
</ResponseField>

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

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

## WebhookSignal

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

```json theme={null}
{ "name": "antidetect_browser", "weight": 60 }
```

<ResponseField name="name" type="string">
  Stable machine slug, for example `vpn`, `datacenter_ip`, `os_mismatch`, or `antidetect_browser`. The flag key for the same detection is `anti_detect_browser`, and History `score_details` names it in free text. Match slugs against the [signal reference](/features/risk-signals). Safe to branch on in application code.
</ResponseField>

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

## ScoreDetail

One entry in the parsed History `score_details` string, and in the deprecated Management Snapshot `Details` array. It is what makes the Risk Score explainable on stored identifications.

```json theme={null}
{ "Value": 15, "Description": "Is VPN" }
```

<ResponseField name="Value" type="integer">
  The weight this risk signal added to the Risk Score. Read the row `score` for the total rather than summing entries; a row whose `score` is 999 is the rate-limit marker, not a sum, so skip it as the [account read](/api/server-api#read-every-identification-of-one-account) does. The [Risk Scoring](/features/risk-scoring) weight table interprets each `Value`. Entries whose `Value` is 0 are diagnostic notes; skip them.
</ResponseField>

<ResponseField name="Description" type="string">
  A free-text description of the signal for people to read. Its wording can change and can carry diagnostic detail, so branch on `Value`, the row `score` and the row's `is_*` flags.
</ResponseField>

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

Each risk signal contributes a fixed weight to the Risk Score, and a higher weight is stronger evidence of masking or spoofing. The full weight table lives on [Risk Scoring](/features/risk-scoring), and the [risk signals](/features/risk-signals) reference explains what each one covers.

<Tip>
  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. The [per-band playbook](/guides/acting-on-risk-score) has a starting policy for each band.
</Tip>

## Snapshot

The object returned by the **deprecated** Management History path (`GET /v1/history/{type}/{value}` on `api.shieldlabs.ai`). A `Snapshot` carries the identity and score fields in **PascalCase** and adds raw network columns. It has no traffic source and no detection flags. That endpoint returns an array of these, newest first. New integrations should use the History API on `account.shieldlabs.ai` (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": "Is VPN" }],
    "LastRequestTime":      "2026-06-16T10:00:00Z"
  }
]
```

The identity, score and signal fields map across three public surfaces. Names and shapes differ, so do not assume one JSON shape:

| Webhook (`data`) | History `account.shieldlabs.ai` | Management snapshot `api.shieldlabs.ai` (deprecated) |
| - | - | - |
| `request_id` | `request_id` | `RequestID` |
| `user_hid` | `user_hid` | `UserHID` |
| `device_id` | `device_id` | `DeviceID` |
| `visitor_id` | `visitor_id` | `VisitorID` |
| `risk_score` | `score` | `Score` |
| `signals[{ name, weight }]` | parsed `score_details` (`{ Value, Description }`) | `Details[{ Value, Description }]` |
| `connection_type` | `connection_type` | `ConnectionType` |
| `public_ip` | `ip`, `country` | `IP`, `Country` |
| `detection_flags.anti_detect_browser` | `is_antidetect` | none: read `Details` |
| `traffic_source` | `traffic_channel`, `referrer_domain`, `entry_url`, `utm_*`, `click_id_type` | none |

Do not treat display labels such as "Anti-detect Browser" as JSON keys. On the webhook, branch on `signals[].name` (`antidetect_browser`) or `detection_flags.anti_detect_browser`; on History rows, on `is_antidetect`. Other Snapshot fields:

<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 device, 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 feed the Risk 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 `user_hid`, `device_id`, `visitor_id`, `ip` (public IP), `request_id`, `session_id` and `cookie_id`. The deprecated Management History path on `api.shieldlabs.ai` accepts the **same seven** types. The full query, `limit` rules, and response shapes are in the [Server API](/api/server-api).

## Profile

The object `GET /v1/profile` returns on `api.shieldlabs.ai`. This read is free.

```json theme={null}
{
  "Domain":    "yourapp.com",
  "Weight":    142850,
  "Callback":  "",
  "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">
  The remaining included volume on your account, in identifications, shared by all your domains. Each identification uses 1; History and profile reads use none. The [Billing](/billing) page has the details.
</ResponseField>

<ResponseField name="Callback" type="string">
  Legacy field kept for older integrations. Webhook delivery uses the endpoints you register in the analytics dashboard under **Integration > Webhooks**.
</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. Read it in full in the [analytics dashboard](https://app.shieldlabs.ai/) under **Integration > API keys**.
</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`). Webhooks are signed with a separate `whsec_…` secret per endpoint, not with the Secret Key. Never put the Secret Key in the browser.
</ResponseField>

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

## High-Risk Events

[High-Risk Events](/features/high-risk-events) are four detections on your users: Multi-accounting, Account sharing, Impossible travel and Account takeover. Each carries Medium or High confidence, a separate axis from the Risk Score, and the confidence depends on the combination of evidence. Events are keyed on the User HID, so pass a hashed User HID with `checkAuthenticatedUser` on every signed-in page. High-Risk Events are available in the analytics dashboard, the API and webhooks.

## Related

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

  <Card title="Server API" icon="server" href="/api/server-api">
    History search, the account read, profile, and `limit` rules.
  </Card>

  <Card title="Risk Score" icon="gauge" href="/features/risk-scoring">
    How the explainable 0-100 Risk Score and the Trusted, Suspicious and Dangerous bands work.
  </Card>

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


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