Skip to main content
When the server finishes scoring an identification, it pushes the result to each configured endpoint as a 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 under data.
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".
The signature travels in the X-Shield-Signature request header, outside the body:

Scored data

Fields inside data 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.
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:
Use it only to confirm URL reachability and signature verification. Do not treat it as a scored identification.

Test delivery

Test on the endpoint in the analytics dashboard (Integration > Webhooks) sends a full identification.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:
The HMAC is computed over the raw request body bytes exactly as received (capture them before any re-encoding), keyed with that endpoint’s signing secret (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.
The signing secret is backend-only. Never put it in the browser, in client-side code, or in the snippet. If a request to your endpoint has a missing or mismatched X-Shield-Signature, reject it. Each endpoint has its own secret, so verify with the secret that belongs to the endpoint that received the call.
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.
A reliable pattern:
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 the risk_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.