Skip to main content

Identification Flow

A ShieldLabs identification is asynchronous by design. The browser snippet collects signals and posts them. The server computes the Risk Score (0–100) a moment later and delivers it two ways: a webhook push and a History API read. There is no synchronous score endpoint: the POST that ingests signals returns an acknowledgment, not the score. The RequestID is the join key that ties the three steps together.
You normally do not call rest.shieldlabs.ai yourself. The JS snippet posts to it automatically. Your server-side work is to receive the webhook and, when you need a guaranteed read, query the History API.

The three steps

1

Snippet posts signals (browser → rest.shieldlabs.ai)

The snippet generates a per-call RequestID (a client UUID) and posts the collected fingerprint to rest.shieldlabs.ai/snapshot/{requestID}?publicKey=…. The response is an acknowledgment (the client IP as a JSON string), not the score.
2

Server scores asynchronously (~1s)

The server enriches the signals (IP intelligence, network fingerprint, real-IP discovery) and computes the Risk Score with an explainable Details array.
3

Score is delivered (webhook + History API)

The server pushes the score to your callback URL as an initial webhook, and may push an update webhook after the real-IP check completes. You can also read the result any time from the History API by request_id.

Step 1: The snapshot POST (acknowledgment, not a score)

The snippet calls this for you. It is documented here so you understand the contract and the RequestID lifecycle.
POST https://rest.shieldlabs.ai/snapshot/{requestID}?publicKey=YOUR_PUBLIC_KEY
Content-Type: application/json

{ ...collected fingerprint (plain JSON or AES-256-GCM encrypted)... }
  • {requestID} is a client-generated UUID, unique per identify call. It is the join key across snapshot → webhook → history.
  • publicKey is your per-domain Public Key. It is safe to expose in the browser.
  • The body is the fingerprint payload. The snippet may send it plain or AES-256-GCM encrypted. Both are accepted.
The response is an acknowledgment, not the result:
"203.0.113.10"
The body is the client IP as a JSON string (HTTP 200). It confirms the signals were received and billed. It does not contain the VisitorID, DeviceID, or Risk Score. Those are computed server-side and delivered in Step 3.
There is no /v1/verify and no synchronous “score” endpoint. Do not block your request flow waiting for a score on this POST. The browser never computes the score; treat this response as a receipt and read the real result from the webhook or History API.
In the browser, the snippet surfaces the same acknowledgment and the requestID through its optional callback, so you can correlate client and server records:
const mod = await import('https://cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_PUBLIC_KEY');

mod.checkAnonymous((serverAck, requestID) => {
  // serverAck = the snapshot acknowledgment (client IP), NOT the score.
  // requestID = the join key. Send it to your backend and look up the score
  // from the webhook you receive, or via the History API.
  fetch('/api/track-request', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ requestID }),
  });
});
See Set up the snippet for the full install and framework examples.

Step 2: Why scoring is asynchronous

The Risk Score is not available the instant the POST lands because the strongest signals need a round of server-side enrichment. This takes roughly one second:

IP intelligence

The server looks up reputation and connection type for the client IP (VPN, proxy, datacenter, privacy relay).

Network fingerprint

A TCP/network fingerprint is fetched and compared against what the browser claims (this is what powers OS-mismatch and the VPN corroboration rule).

Real-IP (STUN) check

The snippet runs a WebRTC real-IP discovery against webrtc.shieldlabs.ai and stun:ice.shieldlabs.ai:3478. The result arrives shortly after the initial POST.
Because these run concurrently and the real-IP check resolves slightly later, ShieldLabs delivers an initial score quickly and then, when the real-IP evidence lands, an optional update. This is why there is no single synchronous endpoint that returns the final number: forcing a synchronous response would mean either waiting on the slowest signal or returning an incomplete score. The VPN “2-of-3” corroboration rule depends on these enrichment steps agreeing, which is part of why the score is computed off the request path.

Step 3: Receiving the score

You get the score two ways. Use both: the webhook for low latency, the History API as the guaranteed-read fallback.

Webhook (push)

The server POSTs the score to your configured callback URL. It arrives in two possible phases, both joined by RequestID:
PhaseTimingDetails content
initial~1s after ingest, before the real-IP resultThe full list of signals that fired
updateAfter the WebRTC real-IP check (optional)Only the delta signals, suppressed if more than ~10s elapsed
The webhook body uses PascalCase field names and is wrapped in a signed envelope:
{
  "Data": {
    "RequestID":       "550e8400-e29b-41d4-a716-446655440000",
    "SessionID":       "7a1b2c3d-4e5f-6789-abcd-ef0123456789",
    "CookieID":        "3f2e1d0c-9b8a-7654-3210-fedcba987654",
    "DeviceID":        "6ba7b810-9dad-11d1-80b4-00c04fd430c9",
    "VisitorID":       "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "IP":              "203.0.113.10",
    "OS":              "Windows",
    "Country":         "US",
    "UserHID":         "e3b0c44298fc1c149afbf4c8996fb924",
    "Score":           35,
    "Details": [
      { "Value": 15, "Description": "VPN" },
      { "Value": 10, "Description": "Datacenter IP" },
      { "Value": 10, "Description": "Timezone Mismatch" }
    ],
    "LastRequestTime": "2026-06-16T10:00:00Z",
    "Phase":           "initial"
  },
  "Assing": "9f1c2b3a4d5e6f70819a2b3c4d5e6f7081920a1b2c3d4e5f60718293a4b5c6d7"
}
Assing is the literal field name in the JSON envelope. It is the HMAC-SHA256 signature of the Data object, keyed with your Secret Key. Verify every webhook before trusting it. A cleaner field name is planned, but Assing is the current reality.
Webhook delivery is at-most-once with no retries (a ~1 second timeout, no backoff, no dead-letter queue). Make your handler idempotent on RequestID and use the History API for anything that must not be missed. Full payload, signature verification in Node/Go/Python, and delivery guarantees are in Webhooks.

History API (read)

You can read the scored result for any RequestID from the Management API. This is the authoritative, pull-based path and the right choice when you cannot risk a dropped webhook.
GET https://api.shieldlabs.ai/{domain}:{secret}/history/request_id/{requestID}?limit=1
The response is an array of snapshots (newest first), a superset of the webhook body that also includes connection and network fields. You can search history by other join keys too: request_id, visitor_id, device_id, user_hid, or ip. Each returned row bills 1 request. See Management API for the full schema and billing rules.

The RequestID lifecycle

RequestID is the single value that lets you stitch the asynchronous pieces together:

Snapshot

Minted client-side as a UUID and sent in the POST path: /snapshot/{requestID}. Returned to your page in the snippet callback.

Webhook

Echoed back as RequestID in the Data body. Both the initial and update phases carry the same RequestID.

History

Queryable as the request_id search type to read the stored snapshot any time.
A reliable pattern:
  1. Capture requestID from the snippet callback and persist it with the user action you are protecting.
  2. When the initial webhook arrives, record Score and Details against that RequestID. Treat the handler as idempotent.
  3. If an update webhook arrives, apply its delta Details and recompute your decision.
  4. If you never receive a webhook (at-most-once delivery), fall back to GET /history/request_id/{requestID}.

What you do with the score

ShieldLabs scores. Your code decides. The Risk Score lands in the 0–100 range with band labels you can act on:
BandRangeRecommended action (a guide, not a rule)
Clean0–9Pass through, no friction
Low10–29Allow, worth logging
Medium30–59Step-up challenge, second look, or review
High60–100Block, review, or require verification
Your application is the actor for allow, challenge, review, or block. Decide on Score + Details + action context, never the number alone: a legitimate user can score high (corporate proxy, VPN, privacy browser). For threshold guidance and worked examples, see Acting on the Risk Score.

Next steps

Webhooks

Full payload, Assing signature verification, phases, and at-most-once delivery.

Management API

History search, the snapshot schema, profile, callback config, and billing.

Risk Score

How the 0–100 explainable score and the Clean / Low / Medium / High bands work.

Identifiers

RequestID, SessionID, CookieID, DeviceID, VisitorID, and UserHID mechanics.