{
  "openapi": "3.1.0",
  "info": {
    "title": "ShieldLabs API",
    "version": "1.0.1",
    "summary": "Identification results and risk scoring for your backend.",
    "description": "The ShieldLabs API gives your backend the result of every identification the ShieldLabs agent\nruns in a browser: the Risk Score, the risk signals behind it, the detection flags and the\nidentifiers (request, visitor, device, session, cookie and User HID) it belongs to.\n\n## How it fits\n\n1. **Browser.** The ShieldLabs agent, loaded from `cdn.shieldlabs.ai`, runs an identification\n   and hands your page a request ID. The browser never receives a Risk Score, a visitor ID or\n   a device ID.\n2. **Your backend.** Your page sends the request ID along with the protected action (signup,\n   login, checkout). Your backend reads the verdict for it from the **History API**, or\n   receives it in a signed `identification.scored` **webhook**.\n3. **Decision.** Your backend acts on `risk_score`, the three risk bands, `detection_flags`\n   and the identifiers, for example by counting how many accounts one `device_id` has used\n   (skipping the `user_hid` values that do not identify a user, listed under Identifiers).\n\nScoring is asynchronous. The webhook usually arrives about 300 ms after the browser check; when\nfollow-up network checks run, it is sent when they finish, at most about 10 seconds later. The\nHistory row appears about 1-3 seconds after the browser call and can be refined for up to about\n10 seconds as follow-up checks finish. Start the identification when the user begins the\nprotected action (for example when the signup form opens), then either poll the History API by\n`request_id` with a short backoff or wait for the webhook. Let one identification authorize one\nprotected action: reject request IDs you have already used and identifications older than your\nfreshness window.\n\n## Hosts and credentials\n\n| API | Host | Paths | Credentials |\n|---|---|---|---|\n| History API | `https://account.shieldlabs.ai` | `/api/v1/...` | `Authorization: Bearer <Private API Key>` (`sec_...`, one per domain) |\n| Management API | `https://api.shieldlabs.ai` | `/v1/...` | `X-Shield-Domain: <registered domain>` and `Authorization: Bearer <Secret Key>` |\n| Health | both hosts | `/health` | none |\n\nEvery operation declares its own server, so generated clients send each call to the right\nhost. The History API serves its paths from the host root: the full URL is\n`https://account.shieldlabs.ai/api/v1/history/{search_type}/{value}`, and a base URL that\nalready ends in `/api` produces `/api/api/v1/...` and a `404`.\n\nThe two credentials are not interchangeable. Keep the Private API Key and the Secret Key on your\nserver; only the Public Key belongs in the browser. All keys are in the analytics dashboard at\nhttps://app.shieldlabs.ai.\n\n## Rate limits\n\n- **History API:** about 15 requests per second per domain, shared by every caller of that\n  domain. Requests over the limit get `429`; there is no ban, so retry after about a second.\n- **Management API:** 15 requests per minute per client IP. The request that goes over the\n  limit starts a 10-minute block, during which every request gets `429`. Never retry a `429`\n  from this API; cache the profile instead.\n- **Health:** not rate limited.\n\nAPI calls and webhook deliveries are free: only identifications made by the browser agent use\nyour included volume.\n\n## Errors\n\nError bodies are not uniform. Branch on the HTTP status first, then try to parse the body as\nJSON whatever its content type.\n\n| API | Status | Body |\n|---|---|---|\n| History API | 401 | JSON text `{\"error\":\"...\"}`, sent as `text/plain` |\n| History API | 429 | `{\"error\":\"too many requests\"}` |\n| History API | 500 | `{\"error\":\"...\"}`. A malformed UUID or IPv4 value always ends here: validate before sending and do not retry it |\n| Management API | 401 | empty |\n| Management API | 400 | a bare JSON string or `null` (deprecated history endpoint) |\n| Management API | 429, 503 | `{\"error\":\"...\"}` |\n| Both | 404 | `404 page not found` as `text/plain` when no route matches |\n| Both | 502, 504 | an HTML page from the edge proxy |\n\nRetry `429` (History API only), `5xx` and network errors with backoff. Do not retry `400`,\n`401` or `404`, nor a History API `500` caused by a malformed value.\n\n## Identifiers\n\n- `request_id`: one identification, created in the browser (UUID v4). It joins the browser\n  call, the webhook and the History row.\n- `session_id`: one visit on one origin (UUID v4).\n- `cookie_id`: first-party browser identifier kept by the agent (UUID v4).\n- `device_id`: server-side device identifier (UUID v5). It survives cleared cookies and private\n  windows. The nil UUID `00000000-0000-0000-0000-000000000000` means no usable device signals.\n- `visitor_id`: server-side visitor identifier (UUID v5), sticky to the device: a new cookie on\n  a known device keeps the visitor ID.\n- `user_hid`: your hashed or pseudonymous account identifier, as passed to the agent.\n  `anonymous` marks anonymous checks; `fail`, `-1` and `unknown` also mean \"no user\". Leave\n  these values, `null` and the empty string out when you count accounts. Hex-encoded hashes\n  are the easiest values to search: see the `value` parameter of `searchHistory` for how to\n  encode other characters.\n\nValidate UUIDs with any version accepted, the nil UUID included.\n\n## Risk Score, risk bands and the 999 marker\n\nThe Risk Score (`risk_score` on webhooks, `score` in the History API) is an integer from 0 to\n100. Search-engine crawlers always score 0. The three risk bands are computed on your side; no\nband field exists on the wire:\n\n| Band | Score |\n|---|---|\n| trusted | 0-29 |\n| suspicious | 30-59 |\n| dangerous | 60-100 |\n\nA value above 100 is not a score. **999** is the rate-limit marker: the visitor's IP went over\nthe ingest rate limit, and the identification carries exactly one signal,\n`{\"name\":\"rate_limited\",\"weight\":999}`, usually with nil identifiers. Treat every value above\n100 as rate limited. One marker is written when the IP goes over the limit; request IDs issued\nwhile it stays blocked get no row and no webhook, so they stay unverified.\n\nBranch on `detection_flags` and the Risk Score. Signal names are for display and logging;\nweights can be negative or change between releases, so never add them up yourself. A missing\nidentification means \"unverified\", never \"clean\".\n\n## Countries, IP addresses and timestamps\n\n- `country` values are English country names from IP intelligence, such as `Germany` or\n  `United States`, or an empty string when unknown.\n- IP fields hold IPv4 addresses. Without an IPv4 address the webhook sends `\"\"` and the History\n  API sends `0.0.0.0`; such identifications cannot be searched by IP.\n- Webhook timestamps (`created_at`, `observed_at`) are RFC 3339 in UTC with up to 9 fractional\n  digits.\n- The History API `created_at` is `YYYY-MM-DD HH:MM:SS.mmm` in UTC without a zone designator;\n  older rows can lack the milliseconds.\n- The Management API `CreatedAt` is RFC 3339 with second precision.\n\n## Webhooks\n\nShieldLabs sends one signed `POST` for each identification to every enabled endpoint of the\ndomain. A delivery has a 1-second timeout and is not retried today; a later release adds\nretries that resend identical bytes. Answer 2xx within a second, process the event\nasynchronously, make the handler idempotent on `data.request_id`, and use the History API for\nguaranteed reads. A History row can be refined after its webhook was sent (its `ver`\nincreases); the webhook is not sent again. See the `identification.scored` and `webhook.ping`\nentries for the signature algorithm.\n\n## Compatibility\n\nIgnore fields you do not know, keep unknown values of string fields (such as new signal names\nor channels) instead of failing, and accept webhook `schema_version` values other than\n`2026-06-01`.\n\nStart free at https://app.shieldlabs.ai. Guides: https://docs.shieldlabs.ai.",
    "contact": {
      "name": "ShieldLabs",
      "url": "https://docs.shieldlabs.ai",
      "email": "contact@shieldlabs.ai"
    },
    "license": {
      "name": "MIT",
      "identifier": "MIT"
    }
  },
  "servers": [
    {
      "url": "https://account.shieldlabs.ai",
      "description": "History API (every operation also declares its own server)"
    },
    {
      "url": "https://api.shieldlabs.ai",
      "description": "Management API (every operation also declares its own server)"
    }
  ],
  "tags": [
    {
      "name": "History API",
      "description": "Read identifications by one identifier on `https://account.shieldlabs.ai` with the Private API Key. The canonical way to read a verdict: by `request_id` right after a protected action, or by `device_id`, `user_hid`, `visitor_id` or `ip` for account-level checks."
    },
    {
      "name": "Management API",
      "description": "Domain profile on `https://api.shieldlabs.ai`, authenticated with the Secret Key and the `X-Shield-Domain` header. Also serves the deprecated history endpoint until 1 January 2027."
    },
    {
      "name": "Health",
      "description": "Unauthenticated liveness checks on both API hosts."
    },
    {
      "name": "Webhooks",
      "description": "Signed events ShieldLabs sends to your webhook endpoints: `identification.scored` for every identification and `webhook.ping` when you verify an endpoint."
    }
  ],
  "externalDocs": {
    "description": "ShieldLabs documentation",
    "url": "https://docs.shieldlabs.ai"
  },
  "paths": {
    "/api/v1/history/{search_type}/{value}": {
      "servers": [
        {
          "url": "https://account.shieldlabs.ai",
          "description": "History API"
        }
      ],
      "get": {
        "operationId": "searchHistory",
        "tags": [
          "History API"
        ],
        "summary": "Search identifications",
        "description": "Returns the identifications of your domain that match one identifier, newest first, together\nwith the total number of matches. The Private API Key selects the domain; identifications from\nits subdomains are included (`domain` holds the host, `site_domain` the registered domain).\n\n**Read one verdict.** After a protected action, search by `request_id` with `limit=1`. The row\nappears about 1-3 seconds after the browser call and can be refined for up to about 10 seconds\nas follow-up network checks finish, so start the identification when the user begins the\naction (for example when the signup form opens), not when the form is submitted. An empty\n`data` array means \"not scored yet\", never \"clean\". Poll with backoff (first try at once, then\nwait 250 ms, 500 ms, 1 s, then steps of about 1.5 s) and treat a `429` inside that loop as\n\"wait longer\". The official server SDKs do this for you.\n\n**Account-level checks.** Search by `device_id`, `user_hid`, `visitor_id` or `ip` to see how\nmany accounts share a device, how many devices one account uses, or what else came from one\nIP address. When you count accounts, skip rows whose `user_hid` is empty or one of the values\nthat do not identify a user: `anonymous`, `fail`, `-1` and `unknown`.\n\n**Validate before sending.** The server does not validate the path: an unknown `search_type`\nreturns the latest identifications of the whole domain unfiltered, a malformed UUID or IPv4\nvalue returns `500`, and a `limit` outside 1-100 silently becomes 20.\n\n**Paging.** Page with `offset` while it is below `total`. Rows are ordered by `created_at`\nonly, so paging while new identifications arrive can repeat or skip rows: deduplicate on\n`request_id`.\n\n**Latest state.** A row can be refined after the webhook was sent, for example when late\nnetwork data re-scores it; its `ver` then increases. The History API always returns the latest\nversion, which makes it the guaranteed read path.\n\nReads are free: they do not use your included identifications.",
        "security": [
          {
            "historyApiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/HistorySearchType"
          },
          {
            "$ref": "#/components/parameters/HistoryValue"
          },
          {
            "$ref": "#/components/parameters/HistoryLimit"
          },
          {
            "$ref": "#/components/parameters/HistoryOffset"
          }
        ],
        "responses": {
          "200": {
            "description": "Matching identifications, newest first. `data` is empty when nothing matched.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HistoryPage"
                },
                "examples": {
                  "page": {
                    "$ref": "#/components/examples/HistoryPage"
                  },
                  "empty": {
                    "$ref": "#/components/examples/HistoryPageEmpty"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/HistoryUnauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/HistoryTooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/HistoryServerError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl \"https://account.shieldlabs.ai/api/v1/history/request_id/a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d?limit=1\" \\\n  -H \"Authorization: Bearer $SHIELDLABS_API_KEY\"\n"
          }
        ]
      }
    },
    "/v1/profile": {
      "servers": [
        {
          "url": "https://api.shieldlabs.ai",
          "description": "Management API"
        }
      ],
      "get": {
        "operationId": "getDomainProfile",
        "tags": [
          "Management API"
        ],
        "summary": "Get the domain profile",
        "description": "Returns the registered domain, the remaining included identifications of the account and the\nmasked keys.\n\n**Credentials.** Send the Secret Key as a Bearer token and the registered domain in\n`X-Shield-Domain`. The domain is matched exactly: send it lowercase, without scheme, path,\ntrailing slash or a leading `www.`.\n\n**Rate limit.** 15 requests per minute per client IP. The request that goes over the limit\nstarts a 10-minute block during which every request to the Management API gets `429`. Call\nthis endpoint sparingly, cache the profile, and never retry a `429`.\n\n`Weight` can be negative when the account is over its included volume. The call is free.",
        "security": [
          {
            "managementSecretKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShieldDomain"
          }
        ],
        "responses": {
          "200": {
            "description": "The domain profile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainProfile"
                },
                "examples": {
                  "profile": {
                    "$ref": "#/components/examples/DomainProfile"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ManagementUnauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/ManagementTooManyRequests"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ManagementServerBusy"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl \"https://api.shieldlabs.ai/v1/profile\" \\\n  -H \"X-Shield-Domain: $SHIELDLABS_DOMAIN\" \\\n  -H \"Authorization: Bearer $SHIELDLABS_SECRET_KEY\"\n"
          }
        ]
      }
    },
    "/v1/history/{type}/{value}": {
      "servers": [
        {
          "url": "https://api.shieldlabs.ai",
          "description": "Management API"
        }
      ],
      "get": {
        "operationId": "searchHistoryDeprecated",
        "tags": [
          "Management API"
        ],
        "summary": "Search history by identifier (deprecated)",
        "deprecated": true,
        "x-sunset": "2027-01-01",
        "description": "**Deprecated.** This endpoint stops working after Sat, 01 Jan 2027 00:00:00 GMT. Use\n`searchHistory` on the History API instead: `https://account.shieldlabs.ai/api/v1/history`.\nEvery answer of this route except `429` and `503` carries `Deprecation: true`, a `Sunset`\nheader and a `Link` header with `rel=\"successor-version\"` pointing there. The plain-text `404`\nfor a path that matches no route and the edge proxy errors do not carry them.\n\nDifferences from the History API: the answer is a bare array of PascalCase objects; only rows\nwhose request host equals `X-Shield-Domain` are returned (no subdomain traffic); `limit`\ndefaults to 100 and there is no `offset`. It uses the Management API credentials and rate limit\n(15 requests per minute per client IP, then a 10-minute block). The call is free.",
        "security": [
          {
            "managementSecretKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShieldDomain"
          },
          {
            "$ref": "#/components/parameters/DeprecatedHistoryType"
          },
          {
            "$ref": "#/components/parameters/DeprecatedHistoryValue"
          },
          {
            "$ref": "#/components/parameters/DeprecatedHistoryLimit"
          }
        ],
        "responses": {
          "200": {
            "description": "Matching identifications, newest first.",
            "headers": {
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              },
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/LegacySnapshot"
                  }
                },
                "examples": {
                  "list": {
                    "$ref": "#/components/examples/LegacySnapshotList"
                  },
                  "empty": {
                    "$ref": "#/components/examples/LegacySnapshotListEmpty"
                  }
                }
              }
            }
          },
          "400": {
            "description": "The value failed validation (a bare JSON string) or the query failed (the JSON literal `null`, for example for an IPv6 `ip` value). Do not retry.",
            "headers": {
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              },
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyErrorMessage"
                },
                "examples": {
                  "invalidUuid": {
                    "$ref": "#/components/examples/ManagementBadRequestUuid"
                  },
                  "invalidIp": {
                    "$ref": "#/components/examples/ManagementBadRequestIp"
                  },
                  "emptyValue": {
                    "$ref": "#/components/examples/ManagementBadRequestEmpty"
                  },
                  "queryFailed": {
                    "$ref": "#/components/examples/ManagementBadRequestNull"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Empty body, no `Content-Type`. Missing or malformed headers, unknown or disabled domain, or a wrong Secret Key. Do not retry.",
            "headers": {
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              },
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            }
          },
          "404": {
            "description": "`application/json`: the `type` is not supported (a bare JSON string); this answer carries the deprecation headers. `text/plain`: no route matches the path, for example because the value is empty or contains `/`; this answer comes from the router and carries no deprecation headers. Do not retry.",
            "headers": {
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              },
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyErrorMessage"
                },
                "examples": {
                  "unsupportedType": {
                    "$ref": "#/components/examples/ManagementUnsupportedType"
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PlainText"
                },
                "examples": {
                  "notFound": {
                    "$ref": "#/components/examples/NotFoundText"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/ManagementTooManyRequests"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ManagementServerBusy"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        }
      }
    },
    "/health": {
      "servers": [
        {
          "url": "https://account.shieldlabs.ai",
          "description": "History API host"
        },
        {
          "url": "https://api.shieldlabs.ai",
          "description": "Management API host"
        }
      ],
      "get": {
        "operationId": "getHealth",
        "tags": [
          "Health"
        ],
        "summary": "Check service health",
        "description": "Liveness check. Returns `{\"status\":\"ok\"}` while the service answers. Available on both API\nhosts: `https://account.shieldlabs.ai/health` for the History API and\n`https://api.shieldlabs.ai/health` for the Management API. No authentication, not rate\nlimited, not billed.",
        "security": [],
        "responses": {
          "200": {
            "description": "The service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthStatus"
                },
                "examples": {
                  "ok": {
                    "$ref": "#/components/examples/HealthOk"
                  }
                }
              }
            }
          },
          "404": {
            "description": "No route matches the path. The health check lives at the host root, so `/api/health` gets this answer. Plain text body.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PlainText"
                },
                "examples": {
                  "notFound": {
                    "$ref": "#/components/examples/NotFoundText"
                  }
                }
              }
            }
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl \"https://account.shieldlabs.ai/health\"\n"
          }
        ]
      }
    }
  },
  "webhooks": {
    "identification.scored": {
      "post": {
        "operationId": "identificationScored",
        "tags": [
          "Webhooks"
        ],
        "summary": "Identification scored",
        "description": "Sent to every enabled webhook endpoint of your domain once for each identification, when its\nscoring is final: usually about 300 ms after the browser check, and at most about 10 seconds\nlater when follow-up network checks run.\n\n**Verify, then parse.** Compute HMAC-SHA256 over the raw request body and compare it with\n`X-Shield-Signature` before you parse the JSON:\n- key: the endpoint's signing secret as UTF-8 bytes, including the `whsec_` prefix (not hex-\n  or base64-decoded, not stripped);\n- message: the exact bytes received; re-serializing parsed JSON changes them (for example, `&`\n  arrives escaped as `\\u0026`);\n- expected header: `sha256=` followed by the lowercase hex digest, compared in constant time.\n\nThere is no timestamp, delivery ID or event-type header. Rotating a secret replaces it at\nonce, so accept both the old and the new secret until your deployment has switched.\n\n**Respond fast.** Answer any 2xx status within 1 second and process the event asynchronously;\ndo not redirect. Today each identification is delivered once per endpoint, with no retries. A\nlater release adds retries that resend identical bytes, so make your handler idempotent on\n`data.request_id`.\n\n**Latest state.** The event is a snapshot taken when scoring finished. The History row can\nstill be refined afterwards (its `ver` increases) and no second event is sent. Use the History\nAPI for guaranteed reads and for the latest state.\n\n**Test deliveries.** The Test button in the analytics dashboard sends a fixed sample with keys\nsorted alphabetically, second-precision timestamps and two-letter country values. Its\n`detection_flags` lack `browser_automation` and `search_bot`: parse missing flags as `false`.\n\nDeliveries are free and do not use your included identifications.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShieldSignature"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The event as compact JSON. Verify the signature over these exact bytes.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IdentificationScoredEvent"
              },
              "examples": {
                "scored": {
                  "$ref": "#/components/examples/IdentificationScored"
                },
                "rateLimited": {
                  "$ref": "#/components/examples/IdentificationScoredRateLimited"
                },
                "testDelivery": {
                  "$ref": "#/components/examples/IdentificationScoredTestDelivery"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Delivery accepted. The response body is ignored."
          },
          "4XX": {
            "description": "Delivery rejected, for example with `401` when the signature does not verify. Any status other than 2xx, and a timeout after 1 second, counts as a failed delivery; failed deliveries are not retried today."
          }
        }
      }
    },
    "webhook.ping": {
      "post": {
        "operationId": "webhookPing",
        "tags": [
          "Webhooks"
        ],
        "summary": "Endpoint verification",
        "description": "Sent when you verify an endpoint in the analytics dashboard. It carries no `data`. A 2xx\nanswer within 5 seconds marks the endpoint as verified; anything else marks the verification\nas failed.\n\nThe body is signed exactly like `identification.scored`. Its keys are sorted alphabetically\nand `created_at` has second precision. Worked example with the test secret\n`whsec_00112233445566778899aabbccddeeff`: the body\n\n```json\n{\"created_at\":\"2026-09-30T12:34:56Z\",\"event_type\":\"webhook.ping\",\"schema_version\":\"2026-06-01\"}\n```\n\narrives with `X-Shield-Signature: sha256=ea2685733d254f7028fb031c4214583b0650de01e6c8c93131236024edd9fdd8`.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShieldSignature"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The ping as compact JSON with sorted keys.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPingEvent"
              },
              "examples": {
                "ping": {
                  "$ref": "#/components/examples/WebhookPing"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Endpoint verified. The response body is ignored."
          },
          "4XX": {
            "description": "Verification failed. Any status other than 2xx, and a timeout after 5 seconds, fails it."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "historyApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "sec_xxxxxxxx-xxxxxxxx-xxxxxxxx",
        "description": "Private API Key of one domain, sent as `Authorization: Bearer <key>`. Keys look like `sec_` followed by three groups of eight lowercase letters or digits separated by `-`. Create and rotate it in the analytics dashboard. It reads the History API of that domain only; keep it on your server."
      },
      "managementSecretKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Secret Key of the domain, sent as `Authorization: Bearer <key>` together with the registered domain in the `X-Shield-Domain` header. Treat the key as opaque. Find it in the analytics dashboard; keep it on your server."
      }
    },
    "parameters": {
      "HistorySearchType": {
        "name": "search_type",
        "in": "path",
        "required": true,
        "description": "Identifier to search by. Only these seven values are supported:\n- `request_id`: one identification (read a verdict);\n- `device_id`: every identification of one device;\n- `user_hid`: every identification of one account;\n- `visitor_id`: every identification of one visitor;\n- `ip`: every identification from one public IPv4 address;\n- `session_id`: every identification of one visit;\n- `cookie_id`: every identification with one browser cookie.\n\nThe server does not reject other values: it ignores them and returns the latest identifications\nof the whole domain, so restrict the value on your side.",
        "schema": {
          "type": "string",
          "enum": [
            "request_id",
            "device_id",
            "user_hid",
            "visitor_id",
            "ip",
            "session_id",
            "cookie_id"
          ]
        },
        "examples": {
          "requestId": {
            "summary": "Read one verdict",
            "value": "request_id"
          },
          "deviceId": {
            "summary": "All identifications of one device",
            "value": "device_id"
          }
        }
      },
      "HistoryValue": {
        "name": "value",
        "in": "path",
        "required": true,
        "description": "Value of the identifier, validated on your side before sending:\n- `request_id`, `device_id`, `visitor_id`, `session_id`, `cookie_id`: a UUID of any version,\n  the nil UUID included, matching\n  `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`. Send it\n  lowercase.\n- `ip`: a dotted IPv4 address. IPv6 addresses cannot be searched.\n- `user_hid`: the exact, case-sensitive User HID as one path segment, encoded the way the\n  server reads it: send the characters `A-Z a-z 0-9 - . _ ~ $ & + , : ; = @` unescaped and\n  percent-encode every other byte of the UTF-8 value as uppercase `%XX`, including\n  `! ' ( ) *`, spaces and `%` itself. The server compares any other encoding literally, so\n  `%40` instead of `@`, or lowercase hex digits, return an empty page instead of the matching\n  rows. Many HTTP clients and generated clients escape `$ & + , : ; = @` in path values: build\n  this path yourself when yours does. A User HID that contains `/` cannot be searched, and most\n  HTTP clients cannot send `.` or `..` because they remove them as dot segments; the pattern\n  rejects these values. Hex-encoded hashes need no escaping at all.\n\nThe server does not validate the value: a malformed UUID or IPv4 address gets a `500`.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "pattern": "^(?:[^/.][^/]*|\\.[^/.][^/]*|\\.\\.[^/]+)$"
        },
        "examples": {
          "requestId": {
            "summary": "A request ID",
            "value": "a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d"
          },
          "ip": {
            "summary": "A public IPv4 address",
            "value": "203.0.113.24"
          },
          "userHid": {
            "summary": "A User HID",
            "value": "9f86d081884c7d659a2feaa0c55ad015"
          }
        }
      },
      "HistoryLimit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Maximum number of identifications to return, from 1 to 100. The server replaces any other value (and a non-numeric one) with 20 instead of clamping it, so validate it on your side.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        },
        "examples": {
          "one": {
            "summary": "Read one verdict",
            "value": 1
          },
          "page": {
            "summary": "A full page",
            "value": 100
          }
        }
      },
      "HistoryOffset": {
        "name": "offset",
        "in": "query",
        "required": false,
        "description": "Number of identifications to skip, for paging. The server treats negative or non-numeric values as 0. Rows are ordered by `created_at` only, so paging while new identifications arrive can repeat or skip rows: deduplicate on `request_id`.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "default": 0
        },
        "examples": {
          "first": {
            "summary": "First page",
            "value": 0
          },
          "second": {
            "summary": "Second page of 100",
            "value": 100
          }
        }
      },
      "ShieldDomain": {
        "name": "X-Shield-Domain",
        "in": "header",
        "required": true,
        "description": "Your registered domain. The server matches it exactly against the domain registered in the analytics dashboard, so send it normalized: lowercase, without scheme, path, trailing slash or a leading `www.` (`https://www.Example.com/` becomes `example.com`). A missing or wrong value gets a `401` with an empty body.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 253
        },
        "examples": {
          "domain": {
            "summary": "A registered domain",
            "value": "example.com"
          }
        }
      },
      "DeprecatedHistoryType": {
        "name": "type",
        "in": "path",
        "required": true,
        "description": "Identifier to search by. Other values get a `404` with a bare JSON string such as `\"auto is not supported\"`.",
        "schema": {
          "type": "string",
          "enum": [
            "request_id",
            "device_id",
            "user_hid",
            "visitor_id",
            "ip",
            "session_id",
            "cookie_id"
          ]
        },
        "examples": {
          "requestId": {
            "summary": "Search by request ID",
            "value": "request_id"
          }
        }
      },
      "DeprecatedHistoryValue": {
        "name": "value",
        "in": "path",
        "required": true,
        "description": "Value of the identifier. UUID types accept a UUID of any version; `ip` must be an IP address (an IPv6 address passes validation but then fails with `400` and a `null` body); `user_hid` is free text.",
        "schema": {
          "type": "string",
          "minLength": 1
        },
        "examples": {
          "requestId": {
            "summary": "A request ID",
            "value": "a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d"
          }
        }
      },
      "DeprecatedHistoryLimit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Maximum number of identifications, from 1 to 100. Any other value becomes 100. There is no `offset`.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 100
        },
        "examples": {
          "ten": {
            "summary": "Ten rows",
            "value": 10
          }
        }
      },
      "ShieldSignature": {
        "name": "X-Shield-Signature",
        "in": "header",
        "required": true,
        "description": "`sha256=` followed by the lowercase hex HMAC-SHA256 of the raw request body:\n- key: your endpoint's signing secret as UTF-8 bytes, the `whsec_` prefix included (not hex- or\n  base64-decoded, not stripped);\n- message: the exact bytes of the body as received.\n\nCompare it with your own digest in constant time, before parsing the JSON. The example values\nare the signatures of the example bodies (in their compact form as sent) with the test secret\n`whsec_00112233445566778899aabbccddeeff`.",
        "schema": {
          "type": "string",
          "pattern": "^sha256=[0-9a-f]{64}$"
        },
        "examples": {
          "identificationScored": {
            "summary": "Signature of the identification.scored example",
            "value": "sha256=397ff9bd26888e9e86addc2d920a8c5b2037251a3a1181f3b4810ca6c5f78062"
          },
          "webhookPing": {
            "summary": "Signature of the webhook.ping example",
            "value": "sha256=ea2685733d254f7028fb031c4214583b0650de01e6c8c93131236024edd9fdd8"
          }
        }
      }
    },
    "schemas": {
      "RequestId": {
        "type": "string",
        "format": "uuid",
        "description": "Identifies one identification. The browser creates it as a UUID v4 and hands it to your page; it is the join key between the browser, the webhook and the History API. The nil UUID appears only on rate-limit marker rows that arrived with a malformed request ID.",
        "examples": [
          "a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d"
        ]
      },
      "SessionId": {
        "type": "string",
        "format": "uuid",
        "description": "One visit on one origin (UUID v4 created in the browser), shared by the open tabs of that origin. The next visit after the last tab closes gets a new session ID. The nil UUID appears on rate-limit marker rows.",
        "examples": [
          "b6c8d0e2-f4a6-4b8c-8d0e-2f4a6b8c0d2e"
        ]
      },
      "CookieId": {
        "type": "string",
        "format": "uuid",
        "description": "First-party browser identifier kept by the ShieldLabs agent (UUID v4). A missing or malformed value is stored as the nil UUID.",
        "examples": [
          "c7d9e1f3-a5b7-4c9d-ae1f-3a5b7c9d1e3f"
        ]
      },
      "DeviceId": {
        "type": "string",
        "format": "uuid",
        "description": "Server-side device identifier (UUID v5). It survives cleared cookies and private windows. The nil UUID `00000000-0000-0000-0000-000000000000` means that no usable device signals were collected (for example on rate-limit marker rows): never group identifications by it.",
        "examples": [
          "d8e0f2a4-b6c8-4d0e-bf2a-4b6c8d0e2f4a",
          "00000000-0000-0000-0000-000000000000"
        ]
      },
      "VisitorId": {
        "type": "string",
        "format": "uuid",
        "description": "Server-side visitor identifier (UUID v5). It is sticky to the device: a new cookie on a known device keeps the existing visitor ID, so clearing cookies usually does not change it. The nil UUID appears on identifications without usable device data, such as rate-limit marker rows.",
        "examples": [
          "e9f1a3b5-c7d9-4e1f-8a3b-5c7d9e1f3a5b"
        ]
      },
      "Ipv4": {
        "type": "string",
        "format": "ipv4",
        "description": "Dotted IPv4 address. `0.0.0.0` when no IPv4 address is known (for example for visitors on IPv6); such identifications cannot be searched by IP.",
        "examples": [
          "203.0.113.24",
          "0.0.0.0"
        ]
      },
      "OperatingSystem": {
        "type": "string",
        "description": "Operating system name, for example `Windows`, `Mac OS X`, `Linux`, `Android`, `IOS (iPhone)`, `IOS (iPad)`, `ChromeOS` or `Unknown`. Open set: display it, do not branch on it.",
        "examples": [
          "Windows",
          "Mac OS X",
          "Android"
        ]
      },
      "Browser": {
        "type": "string",
        "description": "Browser name, for example `Chrome`, `Safari`, `Firefox`, `Microsoft Edge`, `Opera`, `Samsung Internet`, `Brave`, `Chrome (iOS)`, `Safari (iOS)` or `Unknown`. Open set: display it, do not branch on it.",
        "examples": [
          "Chrome",
          "Safari"
        ]
      },
      "DeviceType": {
        "type": "string",
        "description": "Device class from the browser. Known values: `desktop`, `mobile`, `tablet` and `unknown` (the class could not be determined). The set is open: keep values added in later versions and treat them as `unknown`.",
        "x-extensible-enum": [
          "desktop",
          "mobile",
          "tablet",
          "unknown"
        ],
        "examples": [
          "desktop"
        ]
      },
      "Country": {
        "type": "string",
        "description": "English country name from IP intelligence, for example `Germany` or `United States` (not an ISO code). Empty string when the country is unknown.",
        "examples": [
          "Netherlands",
          "United States",
          ""
        ]
      },
      "ConnectionType": {
        "type": "string",
        "description": "How the visitor connected. Known values:\n- `direct`: a regular connection;\n- `mobile`: a mobile carrier network;\n- `vpn`: a VPN;\n- `proxy`: a proxy, datacenter or hosting network (search-engine crawlers are reported here too);\n- `tor`: the Tor network;\n- `privacy_relay`: a privacy relay such as iCloud Private Relay;\n- `browser_vpn_proxy`: a VPN or proxy built into the browser or one of its extensions;\n- `unknown`: not enough data.\n\nThe value can say `vpn` while `detection_flags.vpn` is `false` (IP intelligence classified the\nnetwork, but the scored VPN check did not fire). Branch on `detection_flags` for decisions.\nThe set is open: keep values added in later versions and treat them as `unknown`.",
        "x-extensible-enum": [
          "direct",
          "mobile",
          "vpn",
          "proxy",
          "tor",
          "privacy_relay",
          "browser_vpn_proxy",
          "unknown"
        ],
        "examples": [
          "direct"
        ]
      },
      "RiskScore": {
        "type": "integer",
        "minimum": 0,
        "description": "Risk Score from 0 (no risk found) to 100. Search-engine crawlers always score 0.\n\nRisk bands are computed on your side from the score; no band field exists on the wire:\n- trusted: 0-29\n- suspicious: 30-59\n- dangerous: 60-100\n\nA value above 100 is not a score. `999` is the rate-limit marker: the visitor's IP went over the\ningest rate limit, and the identification carries exactly one signal,\n`{\"name\":\"rate_limited\",\"weight\":999}`, usually with nil identifiers. Treat every value above\n100 as rate limited. One marker is written when the IP goes over the limit; request IDs issued\nwhile it stays blocked get no row and no webhook, so they stay unverified.\n\nThe score usually equals the sum of the signal weights capped at 100, but carried-forward\nverdicts and corrections make that unreliable: never recompute or validate it yourself.",
        "examples": [
          0,
          35,
          80,
          999
        ]
      },
      "ScoreDetail": {
        "type": "object",
        "description": "One entry behind the score, in the PascalCase shape the server stores. `Value` is the weight (0 for informational entries); `Description` is free text for display, never branch on it.",
        "required": [
          "Value",
          "Description"
        ],
        "properties": {
          "Value": {
            "type": "integer",
            "description": "Weight of the entry. Can be negative; 0 for informational entries.",
            "examples": [
              10
            ]
          },
          "Description": {
            "type": "string",
            "description": "Human-readable description, for example `Is proxy` or `Antidetect browser (turn_block)`.",
            "examples": [
              "Is proxy"
            ]
          }
        }
      },
      "HistoryTimestamp": {
        "type": "string",
        "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{3})?$",
        "description": "Time of the identification as `YYYY-MM-DD HH:MM:SS.mmm` in UTC, without a zone designator (not RFC 3339). Older rows can lack the milliseconds.",
        "examples": [
          "2026-09-30 12:34:56.123",
          "2026-09-30 13:05:12"
        ]
      },
      "NetworkClass": {
        "type": "string",
        "description": "Connection class of one IP address from IP intelligence. Known values: `direct`, `mobile`, `vpn`, `proxy`, `tor`, `privacy_relay` and the empty string when unknown. The set is open: keep values added in later versions.",
        "x-extensible-enum": [
          "direct",
          "mobile",
          "vpn",
          "proxy",
          "tor",
          "privacy_relay",
          ""
        ],
        "examples": [
          "direct"
        ]
      },
      "TrafficChannel": {
        "type": "string",
        "description": "Marketing channel of the visit. Known values: `Google Ads`, `Meta`, `TikTok`, `LinkedIn`, `X`, `Pinterest`, `Microsoft Ads`, `Organic Search`, `Search bot`, `Referral`, `Direct`, `Other` and the empty string. Resolved in this order: a click ID, then UTM parameters, then the referrer (search engines give `Organic Search`, social networks give the platform name, other sites give `Referral`), otherwise `Direct`. Search-engine crawlers get `Search bot`. Empty string on identifications without attribution, such as rate-limit marker rows. The set is open: keep values added in later versions and treat them as `Other`.",
        "x-extensible-enum": [
          "Google Ads",
          "Meta",
          "TikTok",
          "LinkedIn",
          "X",
          "Pinterest",
          "Microsoft Ads",
          "Organic Search",
          "Search bot",
          "Referral",
          "Direct",
          "Other",
          ""
        ],
        "examples": [
          "Google Ads",
          "Direct"
        ]
      },
      "TrafficChannelGroup": {
        "type": "string",
        "description": "Group of the marketing channel. Known values: `Paid Search`, `Paid Social`, `Organic`, `Bot`, `Social`, `Referral`, `Direct` and `Other`. History API only; not part of the webhook. The set is open: keep values added in later versions and treat them as `Other`.",
        "x-extensible-enum": [
          "Paid Search",
          "Paid Social",
          "Organic",
          "Bot",
          "Social",
          "Referral",
          "Direct",
          "Other"
        ],
        "examples": [
          "Paid Search"
        ]
      },
      "TrafficReason": {
        "type": "string",
        "description": "Why the channel was chosen. Known values: `gclid_present`, `msclkid_present`, `ttclid_present`, `fbclid_present`, `utm_match`, `referrer_search_engine`, `ip_crawler_detected`, `referrer_social`, `external_referrer` and `no_source_detected`. History API only; not part of the webhook. The set is open: keep values added in later versions.",
        "x-extensible-enum": [
          "gclid_present",
          "msclkid_present",
          "ttclid_present",
          "fbclid_present",
          "utm_match",
          "referrer_search_engine",
          "ip_crawler_detected",
          "referrer_social",
          "external_referrer",
          "no_source_detected"
        ],
        "examples": [
          "gclid_present"
        ]
      },
      "ClickIdType": {
        "type": "string",
        "description": "Ad click identifier found in the landing URL. Known values: `gclid`, `gbraid`, `wbraid`, `msclkid`, `ttclid`, `fbclid` and the empty string when there is none. `fbclid` counts only together with a Meta referrer or a Meta `utm_source`. The set is open: keep values added in later versions.",
        "x-extensible-enum": [
          "gclid",
          "gbraid",
          "wbraid",
          "msclkid",
          "ttclid",
          "fbclid",
          ""
        ],
        "examples": [
          "gclid",
          ""
        ]
      },
      "HistoryRow": {
        "type": "object",
        "additionalProperties": true,
        "description": "One identification as stored, in its latest version. It describes the same identification as a\nwebhook `data` object, with different field names:\n\n| Webhook `data` | History row |\n|---|---|\n| `risk_score` | `score` |\n| `signals` | `score_details` (JSON-encoded string, zero weights included) |\n| `detection_flags` | the `is_*` columns and `check_incomplete` (each column names its flag) |\n| `detection_flags.browser_vpn_proxy` | derive it: `connection_type == \"browser_vpn_proxy\"` |\n| `domain` | `site_domain` when present, otherwise `domain` |\n| `public_ip` | `ip` (`0.0.0.0` instead of `\"\"`) and `country` |\n| `local_ip` | `webrtc_leak_ip` and `webrtc_leak_country` when `webrtc_leak_source` is set and not `none`, otherwise `web_rtc_ip` and `web_rtc_country` |\n| `traffic_source` | `traffic_channel`, `referrer_domain`, `entry_url`, `click_id_type`, `utm_*` (omitted when empty) |\n| `observed_at` (when scoring finished) | `created_at` (when the identification was made) |\n\nThe `ip_mismatch` flag has no column. Rows also carry diagnostic network fields (TCP, MTU and\nSTUN measurements) that are not part of the stable contract: ignore fields you do not know.",
        "required": [
          "request_id",
          "session_id",
          "cookie_id",
          "domain",
          "user_hid",
          "device_id",
          "visitor_id",
          "ip",
          "os",
          "browser",
          "device_type",
          "country",
          "connection_type",
          "score",
          "score_details",
          "created_at",
          "ver",
          "web_rtc_ip",
          "web_rtc_country",
          "web_rtc_connection_type",
          "webrtc_leak_ip",
          "webrtc_leak_country",
          "webrtc_leak_connection_type",
          "webrtc_leak_source",
          "is_vpn",
          "is_tor",
          "is_proxy",
          "is_datacenter",
          "is_abuser",
          "is_privacy_relay",
          "is_stun_not_checked",
          "check_incomplete",
          "is_antidetect",
          "is_os_mismatch",
          "is_os_not_detected",
          "is_timezone_mismatch",
          "is_js_disabled",
          "is_browser_automation",
          "is_incognito",
          "is_search_bot"
        ],
        "properties": {
          "request_id": {
            "$ref": "#/components/schemas/RequestId"
          },
          "session_id": {
            "$ref": "#/components/schemas/SessionId"
          },
          "cookie_id": {
            "$ref": "#/components/schemas/CookieId"
          },
          "domain": {
            "type": "string",
            "description": "Host the identification came from. Can be a subdomain of your registered domain.",
            "examples": [
              "shop.example.com"
            ]
          },
          "site_domain": {
            "type": "string",
            "description": "Your registered domain, present when the identification came from a subdomain. Omitted when empty.",
            "examples": [
              "example.com"
            ]
          },
          "user_hid": {
            "type": "string",
            "description": "User HID exactly as it was passed to the agent (hashed or pseudonymous account identifier). `anonymous` for anonymous checks; `fail`, `-1` and `unknown` also mean \"no user\". Empty string when no value was stored. Leave the empty string and these values out when you count accounts.",
            "examples": [
              "9f86d081884c7d659a2feaa0c55ad015",
              "anonymous"
            ]
          },
          "device_id": {
            "$ref": "#/components/schemas/DeviceId"
          },
          "visitor_id": {
            "$ref": "#/components/schemas/VisitorId"
          },
          "ip": {
            "$ref": "#/components/schemas/Ipv4",
            "description": "Public IPv4 address of the HTTP request; `0.0.0.0` when none (for example IPv6 visitors)."
          },
          "os": {
            "$ref": "#/components/schemas/OperatingSystem"
          },
          "browser": {
            "$ref": "#/components/schemas/Browser"
          },
          "device_type": {
            "$ref": "#/components/schemas/DeviceType"
          },
          "country": {
            "$ref": "#/components/schemas/Country",
            "description": "Country of `ip` as an English country name, or an empty string."
          },
          "connection_type": {
            "$ref": "#/components/schemas/ConnectionType"
          },
          "score": {
            "$ref": "#/components/schemas/RiskScore"
          },
          "score_details": {
            "type": "string",
            "contentMediaType": "application/json",
            "contentSchema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/ScoreDetail"
              }
            },
            "description": "The entries behind `score` as a JSON-encoded **string** holding an array of\n`{\"Value\": <integer>, \"Description\": <string>}`. Parse it before use. Scored entries come\nfirst, followed by informational entries with `Value` 0, which can be long. Empty string\nwhen no details were stored.\n\nThe webhook `signals` are the entries with a non-zero `Value`, in the same order, with each\ndescription turned into a signal name (for example `Is proxy` becomes `proxy`). Descriptions\nare free text for display: never branch on them.",
            "examples": [
              "[{\"Value\":10,\"Description\":\"Is proxy\"},{\"Value\":0,\"Description\":\"Check Incomplete\"}]",
              ""
            ]
          },
          "created_at": {
            "$ref": "#/components/schemas/HistoryTimestamp"
          },
          "ver": {
            "type": "integer",
            "format": "int64",
            "description": "Version of the row in Unix milliseconds. It increases every time the row is refined, for example when late network data re-scores it after the webhook was sent.",
            "examples": [
              1790771696123
            ]
          },
          "web_rtc_ip": {
            "$ref": "#/components/schemas/Ipv4",
            "description": "Local IP address observed by the ShieldLabs network check; `0.0.0.0` when none."
          },
          "web_rtc_country": {
            "$ref": "#/components/schemas/Country",
            "description": "Country of `web_rtc_ip`, or an empty string."
          },
          "web_rtc_connection_type": {
            "$ref": "#/components/schemas/NetworkClass",
            "description": "Connection class of `web_rtc_ip`, or an empty string."
          },
          "webrtc_leak_ip": {
            "$ref": "#/components/schemas/Ipv4",
            "description": "Local network address leaked by the browser; `0.0.0.0` when none."
          },
          "webrtc_leak_country": {
            "$ref": "#/components/schemas/Country",
            "description": "Country of `webrtc_leak_ip`, or an empty string."
          },
          "webrtc_leak_connection_type": {
            "$ref": "#/components/schemas/NetworkClass",
            "description": "Connection class of `webrtc_leak_ip`, or an empty string."
          },
          "webrtc_leak_source": {
            "type": "string",
            "description": "Which check found the local network leak. Known values: `scanner`, `shield`, `none` and the empty string. `none` or an empty string when there is no leak; the webhook `local_ip` then uses `web_rtc_ip`. The set is open: keep values added in later versions.",
            "x-extensible-enum": [
              "scanner",
              "shield",
              "none",
              ""
            ],
            "examples": [
              "none"
            ]
          },
          "is_vpn": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.vpn`."
          },
          "is_tor": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.tor`."
          },
          "is_proxy": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.proxy`."
          },
          "is_datacenter": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.datacenter_ip`."
          },
          "is_abuser": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.abuser`."
          },
          "is_privacy_relay": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.privacy_relay`."
          },
          "is_stun_not_checked": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.stun_not_checked`."
          },
          "check_incomplete": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.check_incomplete`. Always `false` for search-engine crawlers."
          },
          "is_antidetect": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.anti_detect_browser`."
          },
          "is_os_mismatch": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.os_mismatch`."
          },
          "is_os_not_detected": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.os_not_detected`."
          },
          "is_timezone_mismatch": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.timezone_mismatch`."
          },
          "is_js_disabled": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.javascript_disabled`. Always `false` for search-engine crawlers."
          },
          "is_browser_automation": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.browser_automation`."
          },
          "is_incognito": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.incognito`. Always `false` for search-engine crawlers."
          },
          "is_search_bot": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.search_bot`."
          },
          "is_suspicious_paid_click": {
            "type": "boolean",
            "description": "Same meaning as `detection_flags.suspicious_paid_click`. Omitted when `false`."
          },
          "entry_url": {
            "type": "string",
            "description": "Landing page URL without the `#fragment` (webhook `traffic_source.landing_url`). Omitted when empty. It keeps the query string, which can contain personal data.",
            "examples": [
              "https://shop.example.com/signup?utm_source=google&utm_medium=cpc&gclid=abc123"
            ]
          },
          "utm_source": {
            "type": "string",
            "description": "`utm_source`, lowercased. Omitted when empty.",
            "examples": [
              "google"
            ]
          },
          "utm_medium": {
            "type": "string",
            "description": "`utm_medium`, lowercased. Omitted when empty.",
            "examples": [
              "cpc"
            ]
          },
          "utm_campaign": {
            "type": "string",
            "description": "`utm_campaign` as sent. Omitted when empty.",
            "examples": [
              "spring_launch"
            ]
          },
          "utm_content": {
            "type": "string",
            "description": "`utm_content` as sent. Omitted when empty.",
            "examples": [
              "banner_a"
            ]
          },
          "utm_term": {
            "type": "string",
            "description": "`utm_term` as sent. Omitted when empty.",
            "examples": [
              "device intelligence"
            ]
          },
          "traffic_channel": {
            "$ref": "#/components/schemas/TrafficChannel",
            "description": "Marketing channel (webhook `traffic_source.channel`). Omitted when empty."
          },
          "traffic_channel_group": {
            "$ref": "#/components/schemas/TrafficChannelGroup",
            "description": "Group of the marketing channel. Omitted when empty."
          },
          "traffic_reason": {
            "$ref": "#/components/schemas/TrafficReason",
            "description": "Why the channel was chosen. Omitted when empty."
          },
          "referrer_domain": {
            "type": "string",
            "description": "Registrable domain of the referrer without `www.`; the crawler name (for example `GoogleBot`) for search-engine crawlers. Omitted when empty.",
            "examples": [
              "news.example.org"
            ]
          },
          "click_id_type": {
            "$ref": "#/components/schemas/ClickIdType",
            "description": "Ad click identifier type found in the landing URL. Omitted when empty."
          }
        }
      },
      "HistoryPage": {
        "type": "object",
        "description": "One page of identifications, newest first.",
        "required": [
          "data",
          "total"
        ],
        "properties": {
          "data": {
            "type": "array",
            "description": "Identifications on this page, ordered by `created_at` descending. Empty when nothing matched.",
            "items": {
              "$ref": "#/components/schemas/HistoryRow"
            }
          },
          "total": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of identifications that match the search in total, across all pages. Page with `offset` while it is below `total`.",
            "examples": [
              37
            ]
          }
        }
      },
      "ErrorBody": {
        "type": "object",
        "description": "Error object sent by the History API and by the Management API rate and load limits.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message. Branch on the HTTP status, not on this text.",
            "examples": [
              "too many requests"
            ]
          }
        }
      },
      "ErrorBodyText": {
        "type": "string",
        "contentMediaType": "application/json",
        "contentSchema": {
          "$ref": "#/components/schemas/ErrorBody"
        },
        "description": "JSON text followed by a newline, sent with `Content-Type: text/plain; charset=utf-8`. Parse it as JSON: it holds `{\"error\": \"...\"}`.",
        "examples": [
          "{\"error\":\"invalid api key\"}\n"
        ]
      },
      "PlainText": {
        "type": "string",
        "description": "Plain text body.",
        "examples": [
          "404 page not found"
        ]
      },
      "HtmlText": {
        "type": "string",
        "description": "HTML error page from the edge proxy. Do not parse it; branch on the status.",
        "examples": [
          "<html><body><h1>502 Bad Gateway</h1></body></html>"
        ]
      },
      "MaskedKey": {
        "type": "string",
        "pattern": "^(\\*+.{4}|.{0,4})$",
        "description": "A key with every character except the last four replaced by `*`, keeping the original length. Keys of four characters or fewer are returned as they are.",
        "examples": [
          "****************************a3f8"
        ]
      },
      "DomainProfile": {
        "type": "object",
        "description": "Profile of the registered domain. The keys are PascalCase on the wire. Ignore keys you do not know.",
        "required": [
          "Domain",
          "Weight",
          "Callback",
          "PublicKey",
          "Secret",
          "CreatedAt"
        ],
        "properties": {
          "Domain": {
            "type": "string",
            "description": "The registered domain, as sent in `X-Shield-Domain`.",
            "examples": [
              "example.com"
            ]
          },
          "Weight": {
            "type": "integer",
            "description": "Remaining included identifications of the account (shared by its domains). Can be negative when the account is over its included volume.",
            "examples": [
              148230
            ]
          },
          "Callback": {
            "type": "string",
            "description": "Legacy field kept for compatibility, normally an empty string. Webhook deliveries do not use it: configure webhook endpoints in the analytics dashboard.",
            "examples": [
              ""
            ]
          },
          "PublicKey": {
            "$ref": "#/components/schemas/MaskedKey",
            "description": "The domain's Public Key, masked."
          },
          "Secret": {
            "$ref": "#/components/schemas/MaskedKey",
            "description": "The domain's Secret Key, masked."
          },
          "CreatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the domain was registered, RFC 3339 in UTC with second precision. `0001-01-01T00:00:00Z` when unknown.",
            "examples": [
              "2026-01-15T09:00:00Z"
            ]
          }
        }
      },
      "LegacySnapshot": {
        "type": "object",
        "additionalProperties": true,
        "description": "One identification as returned by the deprecated Management API history endpoint (PascalCase keys). Also carries diagnostic network fields that are not part of the stable contract. Use the History API row instead.",
        "required": [
          "RequestID",
          "SessionID",
          "CookieID",
          "DeviceID",
          "VisitorID",
          "IP",
          "ConnectionType",
          "WebRtcHIP",
          "WebRtcCountry",
          "WebRtcConnectionType",
          "OS",
          "Browser",
          "DeviceType",
          "Country",
          "UserHID",
          "Score",
          "Details",
          "LastRequestTime"
        ],
        "properties": {
          "RequestID": {
            "$ref": "#/components/schemas/RequestId"
          },
          "SessionID": {
            "$ref": "#/components/schemas/SessionId"
          },
          "CookieID": {
            "$ref": "#/components/schemas/CookieId"
          },
          "DeviceID": {
            "$ref": "#/components/schemas/DeviceId"
          },
          "VisitorID": {
            "$ref": "#/components/schemas/VisitorId"
          },
          "IP": {
            "$ref": "#/components/schemas/Ipv4",
            "description": "Public IPv4 address of the HTTP request."
          },
          "ConnectionType": {
            "$ref": "#/components/schemas/ConnectionType"
          },
          "WebRtcHIP": {
            "$ref": "#/components/schemas/Ipv4",
            "description": "Local IP address observed by the ShieldLabs network check (not hashed); `0.0.0.0` when none."
          },
          "WebRtcCountry": {
            "$ref": "#/components/schemas/Country",
            "description": "Country of `WebRtcHIP`, or an empty string."
          },
          "WebRtcConnectionType": {
            "$ref": "#/components/schemas/NetworkClass",
            "description": "Connection class of `WebRtcHIP`, or an empty string."
          },
          "OS": {
            "$ref": "#/components/schemas/OperatingSystem"
          },
          "Browser": {
            "$ref": "#/components/schemas/Browser"
          },
          "DeviceType": {
            "$ref": "#/components/schemas/DeviceType"
          },
          "Country": {
            "$ref": "#/components/schemas/Country",
            "description": "Country of `IP` as an English country name, or an empty string."
          },
          "UserHID": {
            "type": "string",
            "description": "User HID as passed to the agent; `anonymous` for anonymous checks.",
            "examples": [
              "9f86d081884c7d659a2feaa0c55ad015"
            ]
          },
          "Score": {
            "$ref": "#/components/schemas/RiskScore"
          },
          "Details": {
            "type": "array",
            "description": "Every entry behind `Score`, informational entries with `Value` 0 included (unlike the History API, this is a parsed array, not a string).",
            "items": {
              "$ref": "#/components/schemas/ScoreDetail"
            }
          },
          "LastRequestTime": {
            "type": "string",
            "format": "date-time",
            "description": "Time of the identification, RFC 3339 with fractional seconds.",
            "examples": [
              "2026-09-30T12:34:56.123Z"
            ]
          }
        }
      },
      "LegacyErrorMessage": {
        "type": [
          "string",
          "null"
        ],
        "description": "A bare JSON string with the error message, or the JSON literal `null` (an unexpected database error, for example for an IPv6 value).",
        "examples": [
          "fail parse uuid",
          null
        ]
      },
      "HealthStatus": {
        "type": "object",
        "description": "Liveness status.",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "ok",
            "description": "Always `ok` when the service answers."
          }
        }
      },
      "SchemaVersion": {
        "type": "string",
        "minLength": 1,
        "description": "Version of the webhook payload contract. Every event sent today carries `2026-06-01`. Accept other values, so that a future version does not break your handler.",
        "examples": [
          "2026-06-01"
        ]
      },
      "Rfc3339Timestamp": {
        "type": "string",
        "format": "date-time",
        "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$",
        "description": "RFC 3339 timestamp in UTC with up to 9 fractional digits (trailing zeros trimmed), for example `2026-09-30T12:34:57.482913041Z`. Parse it with a parser that accepts nanoseconds.",
        "examples": [
          "2026-09-30T12:34:57.482913041Z",
          "2026-09-30T12:34:56Z"
        ]
      },
      "UserHid": {
        "type": [
          "string",
          "null"
        ],
        "description": "User HID: your hashed or pseudonymous account identifier, exactly as it was passed to the\nShieldLabs agent. Pass a hashed value, never a raw email address or database ID.\n\nValues that do not identify a user:\n- `anonymous`: an anonymous check;\n- `fail`: the agent sent no value;\n- `-1` and `unknown`: rows created by ShieldLabs itself, such as rate-limit marker rows.\n\n`null` only when the stored value is an empty string. Leave `null` and the values above out\nwhen you count the accounts of one device, visitor or IP address.",
        "examples": [
          "9f86d081884c7d659a2feaa0c55ad015",
          "anonymous",
          null
        ]
      },
      "Ipv4OrEmpty": {
        "type": "string",
        "pattern": "^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})?$",
        "description": "Dotted IPv4 address, or an empty string when no IPv4 address is known (for example for visitors on IPv6).",
        "examples": [
          "203.0.113.24",
          ""
        ]
      },
      "IpInfo": {
        "type": "object",
        "description": "An IPv4 address and its country. Both keys are always present and can be empty strings.",
        "required": [
          "ip",
          "country"
        ],
        "properties": {
          "ip": {
            "$ref": "#/components/schemas/Ipv4OrEmpty"
          },
          "country": {
            "$ref": "#/components/schemas/Country"
          }
        }
      },
      "TrafficSource": {
        "type": "object",
        "description": "Where the visit came from. All nine keys are always present; values can be empty strings.",
        "required": [
          "channel",
          "referrer_domain",
          "landing_url",
          "click_id_type",
          "utm_source",
          "utm_medium",
          "utm_campaign",
          "utm_content",
          "utm_term"
        ],
        "properties": {
          "channel": {
            "$ref": "#/components/schemas/TrafficChannel"
          },
          "referrer_domain": {
            "type": "string",
            "description": "Registrable domain of the referrer without `www.`. For search-engine crawlers, the crawler name (for example `GoogleBot`).",
            "examples": [
              "google.com"
            ]
          },
          "landing_url": {
            "type": "string",
            "description": "Landing page URL without the `#fragment`. It keeps the query string, which can contain personal data: store it with care.",
            "examples": [
              "https://shop.example.com/signup?utm_source=google&utm_medium=cpc&gclid=abc123"
            ]
          },
          "click_id_type": {
            "$ref": "#/components/schemas/ClickIdType"
          },
          "utm_source": {
            "type": "string",
            "description": "`utm_source` query parameter, lowercased.",
            "examples": [
              "google"
            ]
          },
          "utm_medium": {
            "type": "string",
            "description": "`utm_medium` query parameter, lowercased.",
            "examples": [
              "cpc"
            ]
          },
          "utm_campaign": {
            "type": "string",
            "description": "`utm_campaign` query parameter as sent.",
            "examples": [
              "spring_launch"
            ]
          },
          "utm_content": {
            "type": "string",
            "description": "`utm_content` query parameter as sent.",
            "examples": [
              ""
            ]
          },
          "utm_term": {
            "type": "string",
            "description": "`utm_term` query parameter as sent.",
            "examples": [
              ""
            ]
          }
        }
      },
      "Signal": {
        "type": "object",
        "description": "One weighted risk signal behind the Risk Score. Only signals with a non-zero weight are listed, in scoring order. The same name can appear twice, and weights can be negative.",
        "required": [
          "name",
          "weight"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "description": "Signal name. The set is open: new names can appear at any time, so keep unknown names and\nuse them for display and logging only. Known names:\n- `tor`: the request came through Tor;\n- `vpn`: a VPN was detected;\n- `privacy_relay`: a privacy relay such as iCloud Private Relay;\n- `proxy`: a proxy was detected;\n- `datacenter_ip`: the IP belongs to a datacenter or hosting range;\n- `abuser`: the IP has a record of abuse;\n- `browser_vpn_proxy`: a VPN or proxy inside the browser;\n- `antidetect_browser`: an anti-detect browser (the matching flag is `anti_detect_browser`);\n- `proxy_routed_antidetect`: the network check was routed through a proxy in a way typical\n  for anti-detect browsers;\n- `port_scan_routed_via_proxy`: `proxy_routed_antidetect` carried forward from an earlier\n  identification of the same device and IP;\n- `browser_automation`: browser automation, for example a WebDriver-controlled browser;\n- `javascript_disabled`: JavaScript or the browser APIs the checks need were unavailable;\n- `os_mismatch`: the operating system seen on the network differs from the one the browser\n  reports;\n- `os_not_detected`: the operating system could not be determined;\n- `timezone_mismatch`: the browser timezone differs from the IP location timezone;\n- `stun_not_checked`: the network (STUN) check did not complete;\n- `stun_late_correction`: a late network result arrived; negative weight that cancels\n  `stun_not_checked`;\n- `rate_limited`: the rate-limit marker, weight 999.\n\nA verdict carried forward from an earlier identification of the same device and IP (for\nexample `antidetect_browser`) keeps a name derived from the original signal and can carry a\npartial weight.",
            "examples": [
              "proxy",
              "antidetect_browser"
            ]
          },
          "weight": {
            "type": "integer",
            "description": "Points the signal contributed. Can be negative (`stun_late_correction` is -30) and is 999 for `rate_limited`. Weights can change between releases; never add them up yourself.",
            "examples": [
              10,
              -30
            ]
          }
        }
      },
      "DetectionFlags": {
        "type": "object",
        "description": "Stable yes/no verdicts for the identification. Always all 19 keys. Branch on these flags and on\nthe Risk Score; signal names are for display and logging.\n\nWhen `search_bot` is `true`, `incognito`, `check_incomplete`, `ip_mismatch` and\n`javascript_disabled` are always `false`.",
        "required": [
          "vpn",
          "privacy_relay",
          "browser_vpn_proxy",
          "tor",
          "proxy",
          "datacenter_ip",
          "abuser",
          "os_mismatch",
          "os_not_detected",
          "timezone_mismatch",
          "anti_detect_browser",
          "browser_automation",
          "ip_mismatch",
          "incognito",
          "search_bot",
          "suspicious_paid_click",
          "javascript_disabled",
          "stun_not_checked",
          "check_incomplete"
        ],
        "properties": {
          "vpn": {
            "type": "boolean",
            "description": "A VPN was detected (scored `vpn` signal)."
          },
          "privacy_relay": {
            "type": "boolean",
            "description": "A privacy relay such as iCloud Private Relay was detected."
          },
          "browser_vpn_proxy": {
            "type": "boolean",
            "description": "A VPN or proxy built into the browser or one of its extensions. `true` exactly when `connection_type` is `browser_vpn_proxy`."
          },
          "tor": {
            "type": "boolean",
            "description": "The request came through the Tor network."
          },
          "proxy": {
            "type": "boolean",
            "description": "A proxy was detected."
          },
          "datacenter_ip": {
            "type": "boolean",
            "description": "The public IP belongs to a datacenter or hosting range."
          },
          "abuser": {
            "type": "boolean",
            "description": "The public IP has a record of abuse in IP intelligence."
          },
          "os_mismatch": {
            "type": "boolean",
            "description": "The operating system seen on the network differs from the one the browser reports."
          },
          "os_not_detected": {
            "type": "boolean",
            "description": "The operating system could not be determined from the User-Agent or the network."
          },
          "timezone_mismatch": {
            "type": "boolean",
            "description": "The browser timezone differs from the timezone of the IP location."
          },
          "anti_detect_browser": {
            "type": "boolean",
            "description": "An anti-detect browser was detected."
          },
          "browser_automation": {
            "type": "boolean",
            "description": "Browser automation was detected, for example a WebDriver-controlled browser."
          },
          "ip_mismatch": {
            "type": "boolean",
            "description": "The public IP differs from the local IP found by the browser network check. Informational: it does not add to the score."
          },
          "incognito": {
            "type": "boolean",
            "description": "The browser runs in a private window."
          },
          "search_bot": {
            "type": "boolean",
            "description": "A search-engine crawler. Its Risk Score is always 0."
          },
          "suspicious_paid_click": {
            "type": "boolean",
            "description": "The visit came from a paid ad click (Google Ads, Meta, TikTok, Microsoft Ads, LinkedIn, Pinterest or X) and the Risk Score is 60 or more (the 999 marker included)."
          },
          "javascript_disabled": {
            "type": "boolean",
            "description": "JavaScript, or the browser APIs the checks need, were unavailable."
          },
          "stun_not_checked": {
            "type": "boolean",
            "description": "The browser network (STUN) check did not complete. Cleared again when a late network result arrives."
          },
          "check_incomplete": {
            "type": "boolean",
            "description": "Part of the browser checks timed out, so the verdict rests on partial data. Informational."
          }
        }
      },
      "IdentificationScoredData": {
        "type": "object",
        "description": "The scored identification. Every key is always present (no key is ever omitted); only `user_hid` can be `null`.",
        "required": [
          "request_id",
          "visitor_id",
          "device_id",
          "session_id",
          "cookie_id",
          "user_hid",
          "domain",
          "public_ip",
          "local_ip",
          "connection_type",
          "os",
          "browser",
          "device_type",
          "traffic_source",
          "risk_score",
          "signals",
          "detection_flags",
          "observed_at"
        ],
        "properties": {
          "request_id": {
            "$ref": "#/components/schemas/RequestId"
          },
          "visitor_id": {
            "$ref": "#/components/schemas/VisitorId"
          },
          "device_id": {
            "$ref": "#/components/schemas/DeviceId"
          },
          "session_id": {
            "$ref": "#/components/schemas/SessionId"
          },
          "cookie_id": {
            "$ref": "#/components/schemas/CookieId"
          },
          "user_hid": {
            "$ref": "#/components/schemas/UserHid"
          },
          "domain": {
            "type": "string",
            "description": "Registered domain of your site (the request host when no registered domain matched).",
            "examples": [
              "example.com"
            ]
          },
          "public_ip": {
            "$ref": "#/components/schemas/IpInfo",
            "description": "Public IPv4 address of the HTTP request and its country. `ip` is empty when the request did not arrive over IPv4."
          },
          "local_ip": {
            "$ref": "#/components/schemas/IpInfo",
            "description": "Local IP address found by the browser network check (WebRTC): the leaked address when a local network leak was found, otherwise the address ShieldLabs observed. Both keys are empty when the check found nothing."
          },
          "connection_type": {
            "$ref": "#/components/schemas/ConnectionType"
          },
          "os": {
            "$ref": "#/components/schemas/OperatingSystem"
          },
          "browser": {
            "$ref": "#/components/schemas/Browser"
          },
          "device_type": {
            "$ref": "#/components/schemas/DeviceType"
          },
          "traffic_source": {
            "$ref": "#/components/schemas/TrafficSource"
          },
          "risk_score": {
            "$ref": "#/components/schemas/RiskScore"
          },
          "signals": {
            "type": "array",
            "description": "Weighted risk signals behind `risk_score`, in scoring order. Can be empty. The rate-limit marker carries exactly one entry, `{\"name\":\"rate_limited\",\"weight\":999}`.",
            "items": {
              "$ref": "#/components/schemas/Signal"
            }
          },
          "detection_flags": {
            "$ref": "#/components/schemas/DetectionFlags"
          },
          "observed_at": {
            "$ref": "#/components/schemas/Rfc3339Timestamp",
            "description": "When scoring finished and the event was built (not the page view time); identical to the envelope `created_at`. RFC 3339 in UTC with up to 9 fractional digits."
          }
        }
      },
      "IdentificationScoredEvent": {
        "type": "object",
        "description": "Body of an `identification.scored` delivery. The signature is not part of the body: it arrives in the `X-Shield-Signature` header.",
        "required": [
          "event_type",
          "schema_version",
          "created_at",
          "data"
        ],
        "properties": {
          "event_type": {
            "type": "string",
            "const": "identification.scored",
            "description": "Event type. Ignore events whose type you do not know instead of failing."
          },
          "schema_version": {
            "$ref": "#/components/schemas/SchemaVersion"
          },
          "created_at": {
            "$ref": "#/components/schemas/Rfc3339Timestamp",
            "description": "When the event was built. Equal to `data.observed_at`."
          },
          "data": {
            "$ref": "#/components/schemas/IdentificationScoredData"
          }
        }
      },
      "WebhookPingEvent": {
        "type": "object",
        "description": "Body of a `webhook.ping` delivery, sent when you verify an endpoint. It has no `data`. The keys arrive sorted alphabetically and `created_at` has second precision.",
        "required": [
          "event_type",
          "schema_version",
          "created_at"
        ],
        "properties": {
          "event_type": {
            "type": "string",
            "const": "webhook.ping",
            "description": "Event type."
          },
          "schema_version": {
            "$ref": "#/components/schemas/SchemaVersion"
          },
          "created_at": {
            "$ref": "#/components/schemas/Rfc3339Timestamp",
            "description": "When the ping was sent, with second precision."
          }
        }
      }
    },
    "examples": {
      "HistoryPage": {
        "summary": "Five identifications",
        "description": "One page of five identifications out of 37 matches: a dangerous paid click through a proxy with an anti-detect browser, a trusted anonymous visit, a VPN visit with a local network leak and a late network correction, a rate-limit marker (999) and a search-engine crawler.",
        "value": {
          "data": [
            {
              "request_id": "a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d",
              "session_id": "b6c8d0e2-f4a6-4b8c-8d0e-2f4a6b8c0d2e",
              "cookie_id": "c7d9e1f3-a5b7-4c9d-ae1f-3a5b7c9d1e3f",
              "domain": "shop.example.com",
              "site_domain": "example.com",
              "user_hid": "9f86d081884c7d659a2feaa0c55ad015",
              "device_id": "d8e0f2a4-b6c8-4d0e-bf2a-4b6c8d0e2f4a",
              "visitor_id": "e9f1a3b5-c7d9-4e1f-8a3b-5c7d9e1f3a5b",
              "ip": "203.0.113.24",
              "os": "Windows",
              "browser": "Chrome",
              "device_type": "desktop",
              "country": "Netherlands",
              "connection_type": "proxy",
              "score": 80,
              "score_details": "[{\"Value\":10,\"Description\":\"Is proxy\"},{\"Value\":10,\"Description\":\"Is datacenter\"},{\"Value\":60,\"Description\":\"Antidetect browser (turn_block)\"},{\"Value\":0,\"Description\":\"Check Incomplete\"}]",
              "created_at": "2026-09-30 12:34:56.123",
              "ver": 1790771696123,
              "web_rtc_ip": "198.51.100.23",
              "web_rtc_country": "Germany",
              "web_rtc_connection_type": "direct",
              "scanner_web_rtc_ip": "0.0.0.0",
              "scanner_web_rtc_country": "",
              "scanner_web_rtc_connection_type": "",
              "webrtc_leak_ip": "0.0.0.0",
              "webrtc_leak_country": "",
              "webrtc_leak_connection_type": "",
              "webrtc_leak_source": "none",
              "tcp_mss": 1460,
              "mtu_value": 1500,
              "mtu_hint": "direct",
              "is_vpn": false,
              "is_tor": false,
              "is_proxy": true,
              "is_datacenter": true,
              "is_abuser": false,
              "is_privacy_relay": false,
              "is_stun_not_checked": false,
              "check_incomplete": false,
              "is_antidetect": true,
              "is_os_mismatch": false,
              "is_os_not_detected": false,
              "is_timezone_mismatch": false,
              "is_js_disabled": false,
              "is_browser_automation": false,
              "is_incognito": false,
              "is_search_bot": false,
              "stun_request_seen": true,
              "is_scanner_stun_passed": false,
              "stun_flow_status": "ok",
              "entry_url": "https://shop.example.com/signup?utm_source=google&utm_medium=cpc&gclid=abc123",
              "utm_source": "google",
              "utm_medium": "cpc",
              "traffic_channel": "Google Ads",
              "traffic_channel_group": "Paid Search",
              "traffic_reason": "gclid_present",
              "click_id_type": "gclid",
              "is_suspicious_paid_click": true
            },
            {
              "request_id": "7c1e2f4a-3b6d-4e8f-9a0b-1c2d3e4f5a6b",
              "session_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
              "cookie_id": "f0e1d2c3-b4a5-4968-8776-655443322110",
              "domain": "example.com",
              "user_hid": "anonymous",
              "device_id": "5d9a1f3e-7b2c-5e4d-8f6a-9b0c1d2e3f4a",
              "visitor_id": "3c4d5e6f-7a8b-5c9d-8e0f-1a2b3c4d5e6f",
              "ip": "192.0.2.44",
              "os": "Mac OS X",
              "browser": "Safari",
              "device_type": "desktop",
              "country": "United States",
              "connection_type": "direct",
              "score": 10,
              "score_details": "[{\"Value\":10,\"Description\":\"Browser timezone ≠ IP-timezone\"}]",
              "created_at": "2026-09-30 12:40:01.007",
              "ver": 1790772001007,
              "web_rtc_ip": "192.0.2.44",
              "web_rtc_country": "United States",
              "web_rtc_connection_type": "direct",
              "scanner_web_rtc_ip": "0.0.0.0",
              "scanner_web_rtc_country": "",
              "scanner_web_rtc_connection_type": "",
              "webrtc_leak_ip": "0.0.0.0",
              "webrtc_leak_country": "",
              "webrtc_leak_connection_type": "",
              "webrtc_leak_source": "",
              "tcp_mss": 1460,
              "mtu_value": 1500,
              "mtu_hint": "direct",
              "is_vpn": false,
              "is_tor": false,
              "is_proxy": false,
              "is_datacenter": false,
              "is_abuser": false,
              "is_privacy_relay": false,
              "is_stun_not_checked": false,
              "check_incomplete": false,
              "is_antidetect": false,
              "is_os_mismatch": false,
              "is_os_not_detected": false,
              "is_timezone_mismatch": true,
              "is_js_disabled": false,
              "is_browser_automation": false,
              "is_incognito": true,
              "is_search_bot": false,
              "stun_request_seen": true,
              "is_scanner_stun_passed": false,
              "stun_flow_status": "ok"
            },
            {
              "request_id": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0",
              "session_id": "b0c1d2e3-f4a5-4b6c-9d7e-8f9a0b1c2d3e",
              "cookie_id": "c3d4e5f6-a7b8-4c9d-8e0f-a1b2c3d4e5f6",
              "domain": "example.com",
              "user_hid": "",
              "device_id": "e1f2a3b4-c5d6-5e7f-8a9b-0c1d2e3f4a5b",
              "visitor_id": "d2e3f4a5-b6c7-5d8e-9f0a-1b2c3d4e5f6a",
              "ip": "198.51.100.7",
              "os": "Android",
              "browser": "Chrome",
              "device_type": "mobile",
              "country": "France",
              "connection_type": "vpn",
              "score": 45,
              "score_details": "[{\"Value\":15,\"Description\":\"Is VPN\"},{\"Value\":30,\"Description\":\"Stun is not checked\"},{\"Value\":-30,\"Description\":\"Stun passed (late arrival, corrected)\"},{\"Value\":30,\"Description\":\"Sticky verdict: Stun is not checked (request 11111111-2222-4333-8444-555555555555)\"},{\"Value\":0,\"Description\":\"IP ≠ leakIP (198.51.100.7 ≠ 203.0.113.9, source=scanner)\"}]",
              "created_at": "2026-09-30 13:05:12",
              "ver": 1790773512000,
              "web_rtc_ip": "0.0.0.0",
              "web_rtc_country": "",
              "web_rtc_connection_type": "",
              "scanner_web_rtc_ip": "203.0.113.9",
              "scanner_web_rtc_country": "Spain",
              "scanner_web_rtc_connection_type": "direct",
              "webrtc_leak_ip": "203.0.113.9",
              "webrtc_leak_country": "Spain",
              "webrtc_leak_connection_type": "direct",
              "webrtc_leak_source": "scanner",
              "tcp_mss": 1380,
              "mtu_value": 1420,
              "mtu_hint": "vpn_likely",
              "is_vpn": true,
              "is_tor": false,
              "is_proxy": false,
              "is_datacenter": false,
              "is_abuser": false,
              "is_privacy_relay": false,
              "is_stun_not_checked": true,
              "check_incomplete": true,
              "is_antidetect": false,
              "is_os_mismatch": false,
              "is_os_not_detected": false,
              "is_timezone_mismatch": false,
              "is_js_disabled": false,
              "is_browser_automation": false,
              "is_incognito": false,
              "is_search_bot": false,
              "stun_request_seen": false,
              "is_scanner_stun_passed": true,
              "stun_flow_status": "reply_without_request",
              "entry_url": "https://example.com/pricing",
              "referrer_domain": "news.example.org",
              "traffic_channel": "Referral",
              "traffic_channel_group": "Referral",
              "traffic_reason": "external_referrer"
            },
            {
              "request_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
              "session_id": "00000000-0000-0000-0000-000000000000",
              "cookie_id": "00000000-0000-0000-0000-000000000000",
              "domain": "example.com",
              "user_hid": "-1",
              "device_id": "00000000-0000-0000-0000-000000000000",
              "visitor_id": "00000000-0000-0000-0000-000000000000",
              "ip": "203.0.113.200",
              "os": "Unknown",
              "browser": "Unknown",
              "device_type": "desktop",
              "country": "",
              "connection_type": "unknown",
              "score": 999,
              "score_details": "[{\"Value\":999,\"Description\":\"User has been banned 1H, to many requests\"}]",
              "created_at": "2026-09-30 13:10:00.500",
              "ver": 1790773800500,
              "web_rtc_ip": "0.0.0.0",
              "web_rtc_country": "",
              "web_rtc_connection_type": "",
              "scanner_web_rtc_ip": "0.0.0.0",
              "scanner_web_rtc_country": "",
              "scanner_web_rtc_connection_type": "",
              "webrtc_leak_ip": "0.0.0.0",
              "webrtc_leak_country": "",
              "webrtc_leak_connection_type": "",
              "webrtc_leak_source": "",
              "tcp_mss": 0,
              "mtu_value": 0,
              "mtu_hint": "",
              "is_vpn": false,
              "is_tor": false,
              "is_proxy": false,
              "is_datacenter": false,
              "is_abuser": false,
              "is_privacy_relay": false,
              "is_stun_not_checked": false,
              "check_incomplete": false,
              "is_antidetect": false,
              "is_os_mismatch": false,
              "is_os_not_detected": false,
              "is_timezone_mismatch": false,
              "is_js_disabled": false,
              "is_browser_automation": false,
              "is_incognito": false,
              "is_search_bot": false,
              "stun_request_seen": false,
              "is_scanner_stun_passed": false,
              "stun_flow_status": ""
            },
            {
              "request_id": "4f5e6d7c-8b9a-4c1d-9e2f-3a4b5c6d7e8f",
              "session_id": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d",
              "cookie_id": "6b7c8d9e-0f1a-4b2c-9d3e-4f5a6b7c8d9e",
              "domain": "example.com",
              "user_hid": "anonymous",
              "device_id": "7c8d9e0f-1a2b-5c3d-8e4f-5a6b7c8d9e0f",
              "visitor_id": "8d9e0f1a-2b3c-5d4e-9f5a-6b7c8d9e0f1a",
              "ip": "198.51.100.66",
              "os": "Linux",
              "browser": "Chrome",
              "device_type": "desktop",
              "country": "United States",
              "connection_type": "proxy",
              "score": 0,
              "score_details": "",
              "created_at": "2026-09-30 13:20:30.250",
              "ver": 1790774430250,
              "web_rtc_ip": "203.0.113.77",
              "web_rtc_country": "United States",
              "web_rtc_connection_type": "direct",
              "scanner_web_rtc_ip": "0.0.0.0",
              "scanner_web_rtc_country": "",
              "scanner_web_rtc_connection_type": "",
              "webrtc_leak_ip": "0.0.0.0",
              "webrtc_leak_country": "",
              "webrtc_leak_connection_type": "",
              "webrtc_leak_source": "none",
              "tcp_mss": 1460,
              "mtu_value": 1500,
              "mtu_hint": "direct",
              "is_vpn": false,
              "is_tor": false,
              "is_proxy": false,
              "is_datacenter": false,
              "is_abuser": false,
              "is_privacy_relay": false,
              "is_stun_not_checked": false,
              "check_incomplete": false,
              "is_antidetect": false,
              "is_os_mismatch": false,
              "is_os_not_detected": false,
              "is_timezone_mismatch": false,
              "is_js_disabled": false,
              "is_browser_automation": false,
              "is_incognito": false,
              "is_search_bot": true,
              "stun_request_seen": false,
              "is_scanner_stun_passed": false,
              "stun_flow_status": "",
              "referrer_domain": "GoogleBot",
              "traffic_channel": "Search bot",
              "traffic_channel_group": "Bot",
              "traffic_reason": "ip_crawler_detected"
            }
          ],
          "total": 37
        }
      },
      "HistoryPageEmpty": {
        "summary": "Nothing matched",
        "description": "No identification matched. When searching by `request_id` right after a protected action, this means \"not scored yet\" (or an invalid request ID), never \"clean\".",
        "value": {
          "data": [],
          "total": 0
        }
      },
      "HistoryUnauthorizedMissingHeader": {
        "summary": "Missing or malformed Authorization header",
        "description": "JSON text sent as `text/plain`, followed by a newline.",
        "value": "{\"error\":\"missing or invalid authorization header\"}\n"
      },
      "HistoryUnauthorizedInvalidKey": {
        "summary": "Unknown, deleted or disabled key",
        "description": "JSON text sent as `text/plain`, followed by a newline.",
        "value": "{\"error\":\"invalid api key\"}\n"
      },
      "NotFoundText": {
        "summary": "No route matched",
        "description": "Plain text body of an unrouted path.",
        "value": "404 page not found"
      },
      "HistoryTooManyRequests": {
        "summary": "Soft rate limit reached",
        "description": "More than about 15 requests in the current second for this domain. Retry after about a second.",
        "value": {
          "error": "too many requests"
        }
      },
      "HistoryInvalidValue": {
        "summary": "Malformed UUID value",
        "description": "The raw database error for a value that is not a UUID. It repeats for the same request: validate the value instead of retrying.",
        "value": {
          "error": "code: 53, message: Cannot convert string 'abc' to type UUID"
        }
      },
      "HistoryKeyLookupFailed": {
        "summary": "Key lookup failed",
        "description": "A transient error while checking the key, sent as JSON text. Retry with backoff.",
        "value": "{\"error\":\"internal error\"}\n"
      },
      "BadGatewayHtml": {
        "summary": "Bad gateway",
        "description": "HTML page from the edge proxy.",
        "value": "<html><body><h1>502 Bad Gateway</h1></body></html>"
      },
      "GatewayTimeoutHtml": {
        "summary": "Gateway timeout",
        "description": "HTML page from the edge proxy.",
        "value": "<html><body><h1>504 Gateway Time-out</h1></body></html>"
      },
      "DomainProfile": {
        "summary": "Profile of example.com",
        "description": "A domain with 148,230 remaining included identifications and masked keys.",
        "value": {
          "Domain": "example.com",
          "Weight": 148230,
          "Callback": "",
          "PublicKey": "****************************a3f8",
          "Secret": "****************************9c2d",
          "CreatedAt": "2026-01-15T09:00:00Z"
        }
      },
      "ManagementTooManyRequests": {
        "summary": "Rate limit reached or block active",
        "description": "Do not retry: every request gets this answer until the 10-minute block ends.",
        "value": {
          "error": "too many requests"
        }
      },
      "ManagementServerBusy": {
        "summary": "Too many requests in flight",
        "description": "Retry with backoff.",
        "value": {
          "error": "server is busy"
        }
      },
      "LegacySnapshotList": {
        "summary": "One identification (deprecated shape)",
        "description": "The PascalCase array returned by the deprecated endpoint, for the same identification as the first History API example row.",
        "value": [
          {
            "RequestID": "a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d",
            "SessionID": "b6c8d0e2-f4a6-4b8c-8d0e-2f4a6b8c0d2e",
            "CookieID": "c7d9e1f3-a5b7-4c9d-ae1f-3a5b7c9d1e3f",
            "DeviceID": "d8e0f2a4-b6c8-4d0e-bf2a-4b6c8d0e2f4a",
            "VisitorID": "e9f1a3b5-c7d9-4e1f-8a3b-5c7d9e1f3a5b",
            "IP": "203.0.113.24",
            "ConnectionType": "proxy",
            "TcpMss": 1460,
            "MtuValue": 1500,
            "MtuHint": "direct",
            "WebRtcHIP": "198.51.100.23",
            "WebRtcCountry": "Germany",
            "WebRtcConnectionType": "direct",
            "OS": "Windows",
            "Browser": "Chrome",
            "DeviceType": "desktop",
            "Country": "Netherlands",
            "UserHID": "9f86d081884c7d659a2feaa0c55ad015",
            "Score": 80,
            "Details": [
              {
                "Value": 10,
                "Description": "Is proxy"
              },
              {
                "Value": 10,
                "Description": "Is datacenter"
              },
              {
                "Value": 60,
                "Description": "Antidetect browser (turn_block)"
              }
            ],
            "LastRequestTime": "2026-09-30T12:34:56.123Z"
          }
        ]
      },
      "LegacySnapshotListEmpty": {
        "summary": "Nothing matched",
        "description": "An empty array.",
        "value": []
      },
      "ManagementBadRequestUuid": {
        "summary": "Value is not a UUID",
        "description": "A bare JSON string.",
        "value": "fail parse uuid"
      },
      "ManagementBadRequestIp": {
        "summary": "Value is not an IP address",
        "description": "A bare JSON string.",
        "value": "invalid IP address"
      },
      "ManagementBadRequestEmpty": {
        "summary": "Empty value",
        "description": "A bare JSON string.",
        "value": "value cannot be empty"
      },
      "ManagementBadRequestNull": {
        "summary": "Database error",
        "description": "The JSON literal `null`, for example for an IPv6 `ip` value.",
        "value": null
      },
      "ManagementUnsupportedType": {
        "summary": "Unsupported identifier type",
        "description": "A bare JSON string naming the type that was sent.",
        "value": "auto is not supported"
      },
      "HealthOk": {
        "summary": "Service is up",
        "description": "The only successful answer.",
        "value": {
          "status": "ok"
        }
      },
      "IdentificationScored": {
        "summary": "Dangerous identification from a paid click",
        "description": "Risk Score 80 from a proxy, a datacenter IP and an anti-detect browser, on a visit from a Google Ads click.",
        "value": {
          "event_type": "identification.scored",
          "schema_version": "2026-06-01",
          "created_at": "2026-09-30T12:34:57.482913041Z",
          "data": {
            "request_id": "a5b7c9d1-e3f5-4a7b-9c1d-3e5f7a9b1c3d",
            "visitor_id": "e9f1a3b5-c7d9-4e1f-8a3b-5c7d9e1f3a5b",
            "device_id": "d8e0f2a4-b6c8-4d0e-bf2a-4b6c8d0e2f4a",
            "session_id": "b6c8d0e2-f4a6-4b8c-8d0e-2f4a6b8c0d2e",
            "cookie_id": "c7d9e1f3-a5b7-4c9d-ae1f-3a5b7c9d1e3f",
            "user_hid": "9f86d081884c7d659a2feaa0c55ad015",
            "domain": "example.com",
            "public_ip": {
              "ip": "203.0.113.24",
              "country": "Netherlands"
            },
            "local_ip": {
              "ip": "198.51.100.23",
              "country": "Germany"
            },
            "connection_type": "proxy",
            "os": "Windows",
            "browser": "Chrome",
            "device_type": "desktop",
            "traffic_source": {
              "channel": "Google Ads",
              "referrer_domain": "google.com",
              "landing_url": "https://shop.example.com/signup?utm_source=google&utm_medium=cpc&gclid=abc123",
              "click_id_type": "gclid",
              "utm_source": "google",
              "utm_medium": "cpc",
              "utm_campaign": "",
              "utm_content": "",
              "utm_term": ""
            },
            "risk_score": 80,
            "signals": [
              {
                "name": "proxy",
                "weight": 10
              },
              {
                "name": "datacenter_ip",
                "weight": 10
              },
              {
                "name": "antidetect_browser",
                "weight": 60
              }
            ],
            "detection_flags": {
              "vpn": false,
              "privacy_relay": false,
              "browser_vpn_proxy": false,
              "tor": false,
              "proxy": true,
              "datacenter_ip": true,
              "abuser": false,
              "os_mismatch": false,
              "os_not_detected": false,
              "timezone_mismatch": false,
              "anti_detect_browser": true,
              "browser_automation": false,
              "ip_mismatch": true,
              "incognito": false,
              "search_bot": false,
              "suspicious_paid_click": true,
              "javascript_disabled": false,
              "stun_not_checked": false,
              "check_incomplete": false
            },
            "observed_at": "2026-09-30T12:34:57.482913041Z"
          }
        }
      },
      "IdentificationScoredRateLimited": {
        "summary": "Rate-limit marker (999)",
        "description": "The 999 rate-limit marker: one `rate_limited` signal, nil identifiers and no attribution. It is not a Risk Score.",
        "value": {
          "event_type": "identification.scored",
          "schema_version": "2026-06-01",
          "created_at": "2026-09-30T13:10:00.5Z",
          "data": {
            "request_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
            "visitor_id": "00000000-0000-0000-0000-000000000000",
            "device_id": "00000000-0000-0000-0000-000000000000",
            "session_id": "00000000-0000-0000-0000-000000000000",
            "cookie_id": "00000000-0000-0000-0000-000000000000",
            "user_hid": "-1",
            "domain": "example.com",
            "public_ip": {
              "ip": "203.0.113.200",
              "country": ""
            },
            "local_ip": {
              "ip": "",
              "country": ""
            },
            "connection_type": "unknown",
            "os": "Unknown",
            "browser": "Unknown",
            "device_type": "desktop",
            "traffic_source": {
              "channel": "",
              "referrer_domain": "",
              "landing_url": "",
              "click_id_type": "",
              "utm_source": "",
              "utm_medium": "",
              "utm_campaign": "",
              "utm_content": "",
              "utm_term": ""
            },
            "risk_score": 999,
            "signals": [
              {
                "name": "rate_limited",
                "weight": 999
              }
            ],
            "detection_flags": {
              "vpn": false,
              "privacy_relay": false,
              "browser_vpn_proxy": false,
              "tor": false,
              "proxy": false,
              "datacenter_ip": false,
              "abuser": false,
              "os_mismatch": false,
              "os_not_detected": false,
              "timezone_mismatch": false,
              "anti_detect_browser": false,
              "browser_automation": false,
              "ip_mismatch": false,
              "incognito": false,
              "search_bot": false,
              "suspicious_paid_click": false,
              "javascript_disabled": false,
              "stun_not_checked": false,
              "check_incomplete": false
            },
            "observed_at": "2026-09-30T13:10:00.5Z"
          }
        }
      },
      "IdentificationScoredTestDelivery": {
        "summary": "Test delivery from the analytics dashboard",
        "description": "The fixed sample sent by the Test button: keys sorted alphabetically, second-precision timestamps, two-letter country values and only 17 detection flags (`browser_automation` and `search_bot` are missing). It differs from the schema in exactly those two flags: parse missing flags as `false`.",
        "value": {
          "created_at": "2026-09-30T12:34:56Z",
          "data": {
            "browser": "Chrome",
            "connection_type": "proxy",
            "cookie_id": "2c9d1e8f-4b7a-4c3e-9d2f-1a8b7c6d5e4f",
            "detection_flags": {
              "abuser": true,
              "anti_detect_browser": false,
              "browser_vpn_proxy": false,
              "check_incomplete": false,
              "datacenter_ip": true,
              "incognito": false,
              "ip_mismatch": false,
              "javascript_disabled": false,
              "os_mismatch": false,
              "os_not_detected": false,
              "privacy_relay": false,
              "proxy": true,
              "stun_not_checked": false,
              "suspicious_paid_click": false,
              "timezone_mismatch": false,
              "tor": false,
              "vpn": false
            },
            "device_id": "6f1e2d3c-4b5a-5968-8776-655443322110",
            "device_type": "desktop",
            "domain": "example.com",
            "local_ip": {
              "country": "BY",
              "ip": "198.51.100.10"
            },
            "observed_at": "2026-09-30T12:34:56Z",
            "os": "Windows",
            "public_ip": {
              "country": "BY",
              "ip": "203.0.113.10"
            },
            "request_id": "13f84f05-7c2a-4e9b-9f1d-2a6b8c0e4d11",
            "risk_score": 30,
            "session_id": "3a2b1c0d-9e8f-4a7b-8c6d-5e4f3a2b1c0d",
            "signals": [
              {
                "name": "proxy",
                "weight": 10
              },
              {
                "name": "datacenter_ip",
                "weight": 10
              },
              {
                "name": "abuser",
                "weight": 10
              }
            ],
            "traffic_source": {
              "channel": "Direct",
              "click_id_type": "",
              "landing_url": "https://example.com/",
              "referrer_domain": "",
              "utm_campaign": "",
              "utm_content": "",
              "utm_medium": "",
              "utm_source": "",
              "utm_term": ""
            },
            "user_hid": null,
            "visitor_id": "7a6b5c4d-3e2f-5a1b-9c8d-7e6f5a4b3c2d"
          },
          "event_type": "identification.scored",
          "schema_version": "2026-06-01"
        }
      },
      "WebhookPing": {
        "summary": "Endpoint verification",
        "description": "The ping sent when you verify an endpoint. It has no `data`.",
        "value": {
          "created_at": "2026-09-30T12:34:56Z",
          "event_type": "webhook.ping",
          "schema_version": "2026-06-01"
        }
      }
    },
    "responses": {
      "HistoryUnauthorized": {
        "description": "The `Authorization` header is missing or is not a Bearer token, or the Private API Key is unknown, deleted or belongs to a disabled domain. The body is JSON text sent with `Content-Type: text/plain; charset=utf-8`: parse it as JSON anyway. Do not retry.",
        "content": {
          "text/plain": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBodyText"
            },
            "examples": {
              "missingHeader": {
                "$ref": "#/components/examples/HistoryUnauthorizedMissingHeader"
              },
              "invalidKey": {
                "$ref": "#/components/examples/HistoryUnauthorizedInvalidKey"
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "No route matches the method and path, for example because the base URL repeats part of the path or a path value is empty or contains `/`. Plain text body. Check the URL; do not retry.",
        "content": {
          "text/plain": {
            "schema": {
              "$ref": "#/components/schemas/PlainText"
            },
            "examples": {
              "notFound": {
                "$ref": "#/components/examples/NotFoundText"
              }
            }
          }
        }
      },
      "HistoryTooManyRequests": {
        "description": "More than about 15 requests in the current second for this domain (all callers of the domain share the limit). There is no ban: retry after about a second, with backoff.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "tooManyRequests": {
                "$ref": "#/components/examples/HistoryTooManyRequests"
              }
            }
          }
        }
      },
      "HistoryServerError": {
        "description": "Server error.\n- `application/json`: the query failed. A malformed UUID or IPv4 value always ends here with the\n  raw database message, so validate the path before sending and do not retry such a request.\n  Other failures are transient.\n- `text/plain` (JSON text): the key lookup failed. Transient: retry with backoff.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "invalidValue": {
                "$ref": "#/components/examples/HistoryInvalidValue"
              }
            }
          },
          "text/plain": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBodyText"
            },
            "examples": {
              "keyLookupFailed": {
                "$ref": "#/components/examples/HistoryKeyLookupFailed"
              }
            }
          }
        }
      },
      "BadGateway": {
        "description": "The edge proxy could not reach the service. HTML body. Retry with backoff.",
        "content": {
          "text/html": {
            "schema": {
              "$ref": "#/components/schemas/HtmlText"
            },
            "examples": {
              "badGateway": {
                "$ref": "#/components/examples/BadGatewayHtml"
              }
            }
          }
        }
      },
      "GatewayTimeout": {
        "description": "The service did not answer the edge proxy in time. HTML body. Retry with backoff.",
        "content": {
          "text/html": {
            "schema": {
              "$ref": "#/components/schemas/HtmlText"
            },
            "examples": {
              "gatewayTimeout": {
                "$ref": "#/components/examples/GatewayTimeoutHtml"
              }
            }
          }
        }
      },
      "ManagementUnauthorized": {
        "description": "Empty body, no `Content-Type`. `X-Shield-Domain` or `Authorization` is missing or malformed, the domain is unknown or disabled, or the Secret Key is wrong. Do not retry."
      },
      "ManagementTooManyRequests": {
        "description": "More than 15 requests in the current minute from your IP, or a 10-minute block is active. The request that exceeds the limit starts the block, and every request during it gets this answer. Do not retry: wait for the block to end and cache results to stay under the limit.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "tooManyRequests": {
                "$ref": "#/components/examples/ManagementTooManyRequests"
              }
            }
          }
        }
      },
      "ManagementServerBusy": {
        "description": "Too many requests are in flight on the server. Retry with backoff.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorBody"
            },
            "examples": {
              "serverBusy": {
                "$ref": "#/components/examples/ManagementServerBusy"
              }
            }
          }
        }
      }
    },
    "headers": {
      "Deprecation": {
        "description": "Marks the endpoint as deprecated. The value is the literal `true`.",
        "schema": {
          "type": "string",
          "const": "true"
        },
        "examples": {
          "deprecated": {
            "summary": "Deprecated endpoint",
            "value": "true"
          }
        }
      },
      "Sunset": {
        "description": "HTTP date after which the endpoint stops working.",
        "schema": {
          "type": "string",
          "const": "Sat, 01 Jan 2027 00:00:00 GMT"
        },
        "examples": {
          "sunset": {
            "summary": "Sunset date",
            "value": "Sat, 01 Jan 2027 00:00:00 GMT"
          }
        }
      },
      "Link": {
        "description": "Points to the replacement endpoint with `rel=\"successor-version\"`.",
        "schema": {
          "type": "string",
          "const": "<https://account.shieldlabs.ai/api/v1/history>; rel=\"successor-version\""
        },
        "examples": {
          "successor": {
            "summary": "Successor endpoint",
            "value": "<https://account.shieldlabs.ai/api/v1/history>; rel=\"successor-version\""
          }
        }
      }
    }
  }
}
