POST. This is the canonical, low-latency way to receive the Risk Score and the risk signals behind it.
Each identification.scored delivery is one identification, the event layer under your users, devices, visitors and IPs. It carries exactly one visitor, one device and one public IP, and at most one user and one local IP. Group deliveries by user_hid, device_id, visitor_id, public_ip.ip and local_ip.ip to follow an account, a device, a visitor or an IP over time; the History API reads the same history on demand by user_hid, device_id, visitor_id or public ip. High-Risk Events are detected on your users and are available in the analytics dashboard, the API and webhooks.
This page is the reference: the envelope schema, delivery timing, and how to verify the signature. The webhook setup guide gives a step-by-step walkthrough of configuring and testing an endpoint.
You do not poll for webhooks. The server sends them to the endpoints you configure per domain (up to 10) in the analytics dashboard under Integration > Webhooks (see the setup guide and Integration). Delivery is at-most-once with no retries, so pair it with a History API read when you cannot afford to miss a result.
Envelope
Each POST body is a JSON envelope in snake_case. Event metadata lives at the top level; the scored identification lives underdata.
string
Discriminator for the delivery. Every scored identification uses
identification.scored. Service deliveries use webhook.ping (Verify on an endpoint). Ignore unknown types until you add support.string
Contract version, for example
2026-06-01. Check this before parsing data so you can branch when ShieldLabs ships a new schema.string (ISO 8601)
When this webhook event was created and signed, in RFC 3339 form.
object
Present on
identification.scored events. Absent on webhook.ping. See Scored data below.Scored delivery (identification.scored)
Each scored identification produces one webhook per enabled endpoint. This example is an anonymous check, so user_hid is "anonymous".
X-Shield-Signature request header, outside the body:
Scored data
Fields insidedata on identification.scored events.
string (UUID)
The client-generated UUID of this identification. Join key across the snippet call, the webhook, and the History API. Make your handler idempotent on this value.
string (UUID)
The Session ID: the browsing session, written by the snippet to
localStorage (legacy sessionStorage keys are read once and migrated), so it is shared across tabs rather than tied to one tab.string (UUID)
The Cookie ID: a first-party cookie /
localStorage identifier minted in the browser. Lost when the user clears cookies or storage.string (UUID)
The device. Server-derived from device intelligence, so it holds through cleared cookies, incognito mode and IP changes; another browser on the same machine gets its own Device ID. Read every identification of a device from the History API by
device_id. The Identifiers reference explains the model.string (UUID)
The visitor: one device plus one cookie, server-derived from
device_id and cookie_id. Changes when the cookie is cleared, so one device_id can have several visitor_id values. Read a visitor’s identifications by visitor_id.string | null
The User HID: your account key, passed with
checkAuthenticatedUser. Always pass a hashed or pseudonymous value, never a raw email or user id. Anonymous checks (checkAnonymous) carry the literal anonymous; null appears when an empty string is passed, and on the fixed sample of a test delivery. Pass a hashed User HID with checkAuthenticatedUser on every signed-in page. Users, account-level risk and all four High-Risk Events are built on it. Read an account’s identifications by user_hid.string
The site domain this identification belongs to (your registered domain key).
object
The public IP of this identification, resolved for this HTTP request. Read every identification from one public IP from the History API by
ip.object
The local IP: the address the browser itself reports, captured by an optional follow-up network check when available. It can differ from
public_ip when the user is behind a VPN, a proxy, or a split tunnel. In the example above, public_ip is a US proxy exit (203.0.113.42, US) while local_ip resolves to the user’s own network in Germany (198.51.100.23, DE); because the two addresses differ, detection_flags.ip_mismatch is true. ShieldLabs returns both IPs so you can compare them; the difference is informational and adds nothing to the Risk Score. To group accounts or devices by local IP, keep local_ip.ip from each webhook; the History API searches by the public ip. In the analytics dashboard, local IPs appear on user, device, visitor and IP cards as linked local IPs, with the identifications behind each.string
The operating system derived for the device (for example
Windows, Mac OS X, or IOS (iPhone)). May be empty when it cannot be determined.string
The browser family detected for this identification, for example
Chrome or Firefox.string
Device class:
desktop, mobile, or tablet.string
The detected network connection type, one of
direct, mobile, vpn, proxy, tor, privacy_relay, browser_vpn_proxy, or unknown (when the type could not be resolved).integer
The Risk Score of this identification, an integer from 0 to 100. Higher means riskier: more likely masked, spoofed, or abusive. The payload carries the number, with no band field. Map it to a band in your backend: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. The only value above 100 is
999, the rate-limit marker written during a per-IP ban, so guard risk_score > 100 before reading the band.array of objects
The full explainable breakdown: every risk signal with a non-zero weight, as
{ "name": "<slug>", "weight": <int> }. A slug can appear twice when an earlier verdict is carried forward with a partial weight, so branch on slugs and detection_flags and read risk_score for the total.object
Resolved traffic attribution for the identification.
object
Boolean flags for each detection dimension. Use these for quick branching; use
signals for the explainable Risk Score breakdown. Not every flag contributes to the Risk Score: some are informational (for example ip_mismatch and incognito). The scored subset and their weights are in the Risk Scoring table.Bots on the wire:
search_bot marks a known search-engine crawler (a good bot, with risk_score set to 0), browser_automation marks an automation-controlled browser (a bad bot, weight 60), and javascript_disabled marks a headless or automated client (weight 90).string (ISO 8601)
When the identification was scored and this payload was built, in RFC 3339 / ISO 8601 form. Same value as
created_at.Branch on
risk_score, stable signal name slugs, and detection_flags, not on the free-text Description entries of History API score_details.Delivery timing
The server waits at most about 10 seconds after the check for optional follow-up network checks. If a follow-up never arrives, the webhook is still sent with the best Risk Score available at the deadline.
Full background is in the Identification Flow.
Service events
Verify ping (webhook.ping)
Verify on an endpoint in the analytics dashboard (Integration > Webhooks) sends a minimal envelope with no data. A 2xx answer marks the endpoint active:
Test delivery
Test on the endpoint in the analytics dashboard (Integration > Webhooks) sends a fullidentification.scored envelope with sample data. The sample always carries the same request_id and a null user_hid. Parse it like production traffic; deduplicate on data.request_id if you replay tests.
Verification
Verify the signature on every webhook before acting on it. The recipe:whsec_…). Hex-encode it, prefix with sha256=, and constant-time compare against the X-Shield-Signature header. Re-serializing the parsed JSON changes the bytes and the signature will not match.
Copy-paste verification handlers for Node, Go, and Python live in the webhook setup guide.
The analytics dashboard also shows verification samples in six languages under Integration > Webhooks; Integration describes the screen.
Delivery guarantees
Webhook delivery is intentionally lightweight. Design your handler around these properties.At-most-once
Each identification produces one send attempt per endpoint. There is no retry, no backoff, and no dead-letter queue. A dropped network connection means that webhook is gone.
1-second timeout
The sender waits one second for your endpoint, then moves on. Acknowledge with a fast
2xx and do heavy work asynchronously, off the request path.Idempotent on request_id
Key your writes on
data.request_id so a repeated delivery, such as a replayed test, is a no-op.Read fallback
For anything you cannot afford to miss, read the result from the History API by
request_id. That is the guaranteed, pull-based path.1
Persist the request_id early
Capture
requestID from the snippet callback and store it with the user action you are protecting.2
Apply the webhook
On delivery, verify the signature, check
event_type === "identification.scored", then record data.risk_score and data.signals against data.request_id. Treat the write as idempotent.3
Fall back to a read
If no webhook arrives within about 10 seconds, call the History API by
request_id to read the stored identification.Acting on the payload
The webhook gives you therisk_score of one identification and the risk signals behind it. Before a sensitive action, read the account behind it too: its identifications from the History API by user_hid give its worst band and the devices and IPs it is linked to. When a High-Risk Event arrives for the user, or when you review it on the user’s card in the analytics dashboard, act on the account; the Risk Score and risk signals of this identification remain the input at signup, login, checkout or withdrawal. You choose the action for each case (allow, step up, review or block) and act on the result in your backend. The per-band playbook covers each band.
Next steps
Set up a webhook
The tutorial: configure your endpoints, test them, and go live.
Data Models
The full envelope and Snapshot schemas, and the identity each identifier maps to.
Server API
Read one identification by request ID, or every identification of one account, device, visitor or public IP.
Risk Score
How the explainable 0-100 Risk Score and the Trusted, Suspicious and Dangerous bands work.