> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shieldlabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Identification Flow

> How an identification is scored and reaches your backend by webhook and through the History API.

An identification is one check by the snippet, the event layer under your users, devices, visitors and IPs. ShieldLabs scores it **asynchronously**: the browser snippet collects signals and posts them, and the POST returns an acknowledgment. The server computes the Risk Score (0 to 100) in about 300 ms and delivers it two ways: a **webhook** push and a **History API** read. Each identification belongs to one visitor, one device and one public IP, and to one of your users when the page passes a hashed User HID.

The `request_id` ties the three steps together.

<Info>
  You normally do not call `rest.shieldlabs.ai` yourself. The [JS snippet](/setup/snippet) posts to it automatically. Your server-side work is to receive the [webhook](/api/webhooks) and, when you need a guaranteed read or the account behind an identification, query the [History API](/api/server-api).
</Info>

## The three steps

<Steps>
  <Step title="Snippet posts signals (browser → rest.shieldlabs.ai)">
    Load the current [snippet](/setup/snippet) from the official CDN. It generates a per-call request ID (a client UUID) and posts collected signals to `rest.shieldlabs.ai`. The response is an **acknowledgment** (the client IP as a JSON string), not the Risk Score.
  </Step>

  <Step title="Server scores asynchronously (about 300 ms)">
    The server computes the [Risk Score](/features/risk-scoring). The webhook carries the breakdown in `data.signals` as `{ name, weight }` (stable slugs, for example `antidetect_browser`). History API rows carry the same breakdown in `score_details`, a JSON string of `{ Value, Description }` entries.
  </Step>

  <Step title="The result is delivered (webhook and History API)">
    The server pushes **one** final [webhook](/api/webhooks) per identification (typically about 300 ms after the check; at most about 10 seconds when follow-up network checks run). You can also read the result any time from the [History API](/api/server-api) by `request_id`, and every identification of the account by `user_hid`.
  </Step>
</Steps>

```mermaid theme={null}
sequenceDiagram
    participant B as Browser (snippet)
    participant R as rest.shieldlabs.ai
    participant A as account.shieldlabs.ai
    participant S as ShieldLabs scoring
    participant Y as Your server

    B->>R: POST signals (snippet, automatic)
    R-->>B: 200 OK acknowledgment (not the Risk Score)
    Note over S: score (about 300 ms, at most about 10 s)
    S->>Y: webhook envelope  event_type + data (risk_score, signals, once)
    Y->>A: GET /api/v1/history/request_id/{requestID}  (guaranteed read)
    Y->>A: GET /api/v1/history/user_hid/{userHid}  (the account's identifications)
```

## Step 1: The identification POST (acknowledgment, not a Risk Score)

The [JS snippet](/setup/snippet) calls ingest for you. Load it from `https://cdn.shieldlabs.ai`. Do not self-host, mirror, bundle or pin copies of the snippet.

* `{requestID}` is a **client-generated UUID**, unique per identification. It is the join key across the snippet call, the webhook and History.
* `publicKey` is your per-domain [Public Key](/setup/keys). It is safe to expose in the browser.

**The ingest response is an acknowledgment, not the result:**

```json theme={null}
"203.0.113.10"
```

The body is the client IP as a JSON string (HTTP `200`). It confirms the signals were received and the identification counted. The Visitor ID, Device ID and Risk Score are computed on the server and delivered in Step 3.

<Warning>
  This response is a receipt, not the Risk Score; read the result from the webhook or History API.
</Warning>

In the browser, the snippet surfaces the `requestID` through optional `onInitialized`, so you can correlate client and server records. The ingest HTTP body stays a receipt and is not copied onto that object.

```js theme={null}
const mod = await import('https://cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_PUBLIC_KEY');

const report = (result) => {
  if (result.status !== 'initialized') return;
  // requestID is the join key. Send it to your backend with the action it belongs to,
  // then read the result from the webhook or through the History API.
  fetch('/api/track-request', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ requestID: result.requestID }),
  });
};

// hashedUserId: your hashed account id on signed-in pages, otherwise null.
if (hashedUserId) {
  // Signed-in page: the hashed User HID ties this identification to the account.
  mod.checkAuthenticatedUser(hashedUserId, { onInitialized: report });
} else {
  mod.checkAnonymous({ onInitialized: report });
}
```

Within one visit (while a page of your site stays open in the browser, across route changes in a single-page app and across open tabs), `checkAnonymous` and `checkAuthenticatedUser` run at most one identification every five minutes for the same user. A call inside that window posts nothing, counts nothing, and its `onInitialized` handler receives `{ status: "not_initialized" }`. On a multi-page site, a full page load in the only open tab can start a new visit with its own identification. `forceCheckAnonymous` and `forceCheckAuthenticatedUser` run an identification every time, keep the current Session ID and restart the five-minute window. The [snippet install guide](/setup/snippet) has the full method list and framework examples.

## Step 2: Why scoring is asynchronous

Scoring runs on the server after ingest acknowledges the identification and typically takes about 300 ms. When follow-up network checks run, ShieldLabs waits for them for at most about 10 seconds, then delivers **one** final webhook per identification.

## Step 3: Receiving the result

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

### Webhook (push)

The server `POST`s the result to each enabled webhook endpoint **once per identification**, joined by `request_id`:

| Timing | Content |
| - | - |
| About 300 ms (no follow-up network check) | Full `risk_score` and `signals` |
| At most about **10 s** (follow-up network checks run) | Final Risk Score when the follow-up checks finish, or the best available at the deadline |

The webhook body is a signed snake\_case **envelope**. The signature travels in the `X-Shield-Signature` header. The example below is shortened; every field is in [WebhookEvent](/api/models#webhookevent).

```http theme={null}
POST https://your-server.com/webhook
Content-Type: application/json
X-Shield-Signature: sha256=9f1c2b3a4d5e6f70819a2b3c4d5e6f7081920a1b2c3d4e5f60718293a4b5c6d7

{
  "event_type": "identification.scored",
  "schema_version": "2026-06-01",
  "created_at": "2026-06-16T10:00:45Z",
  "data": {
    "request_id": "550e8400-e29b-41d4-a716-446655440000",
    "user_hid": "e3b0c44298fc1c149afbf4c8996fb924",
    "risk_score": 30,
    "signals": [
      { "name": "proxy", "weight": 10 },
      { "name": "datacenter_ip", "weight": 10 },
      { "name": "abuser", "weight": 10 }
    ],
    "observed_at": "2026-06-16T10:00:45Z"
  }
}
```

<Note>
  Verify `X-Shield-Signature` on the raw request body, keyed with that endpoint's `whsec_…` signing secret (not the domain Secret Key). See the [verification recipe](/api/webhooks#verification).
</Note>

Webhook delivery is **at-most-once with no retries** (a 1-second send timeout, no backoff, no dead-letter queue). Make your handler idempotent on `request_id` and use the History API for anything that must not be missed. Full payload and delivery guarantees are in [Webhooks](/api/webhooks); copy-paste verification handlers in Node, Go and Python are in the [webhook setup guide](/setup/webhooks#verify-the-signature).

### History API (read)

You can read the scored result for any request ID from the [History API](/api/server-api). This is the authoritative, pull-based path and the right choice when you cannot risk a dropped webhook.

```http theme={null}
GET https://account.shieldlabs.ai/api/v1/history/request_id/{requestID}?limit=1
Authorization: Bearer sec_your_private_api_key
```

The response is a `{ data, total }` envelope of identifications, newest first. Search by `user_hid` to read an account's identifications, or by `device_id`, `visitor_id`, `ip`, `request_id`, `session_id` or `cookie_id`. History reads never use your included identifications. The full schema is in the [Server API](/api/server-api) reference.

## The request ID lifecycle

The request ID is the single value that lets you stitch the asynchronous pieces together:

<CardGroup cols={3}>
  <Card title="Snippet call" icon="upload">
    Minted in the browser as a UUID when the snippet runs. Returned to your page in the `onInitialized` callback.
  </Card>

  <Card title="Webhook" icon="bolt">
    Echoed back as `request_id`. One scored POST per identification and endpoint.
  </Card>

  <Card title="History" icon="magnifying-glass">
    Queryable as the `request_id` search type to read the stored identification any time.
  </Card>
</CardGroup>

Persist the `requestID` early, record `risk_score` and `signals` idempotently when the webhook arrives, and fall back to `GET /api/v1/history/request_id/{requestID}` on `account.shieldlabs.ai` if it has not arrived within about 10 seconds. The full reliability pattern, with signature verification, is in [Webhooks](/api/webhooks#delivery-guarantees).

## From the identification to the account

Each identification carries the keys of the identities it belongs to: `user_hid` (your account), `device_id`, `visitor_id`, `public_ip` and `local_ip`. 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.

To see an account as a whole, [read its identifications](/api/server-api#read-every-identification-of-one-account) from the History API by `user_hid`. The worst band across them is the account's risk, and the Device IDs, Visitor IDs and public IPs in those rows are what the account is linked to. Devices, visitors and public IPs read the same way by `device_id`, `visitor_id` and `ip`. High-Risk Events (Multi-accounting, Account sharing, Impossible travel and Account takeover) are detected on your users, each at Medium or High confidence, and are available in the analytics dashboard, the API and webhooks.

The same account, with its band, its High-Risk Events and each linked device, visitor and IP with the band of the identifications it shares with the account, is on the user's card in the analytics dashboard ([User, device, visitor and IP cards](/dashboard/entity-card)).

## What you do with the result

The Risk Score lands in the 0 to 100 range (999 marks a rate-limited identification) and falls into three [Risk Score bands](/features/risk-scoring): Trusted 0-29, Suspicious 30-59, Dangerous 60-100. The payload carries the number, so map it to a band in your backend. At signup, login, checkout or a payout, read the Risk Score of this identification together with the account behind it. The Risk Score and risk signals of this identification remain the input at that moment. When a High-Risk Event arrives for a user, or when you review it in the analytics dashboard, act on the account. Read a high Risk Score together with its named risk signals (`signals` on the webhook, `score_details` on the History API) and the user's history. A real customer on a corporate VPN can reach the Suspicious band; the signals show why, and you choose the action for each case: allow, step up, review or block. The [per-band playbook](/guides/acting-on-risk-score) gives starting policies and worked examples.

## Next steps

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

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

  <Card title="Users, devices, visitors and IPs" icon="users" href="/concepts/entities">
    The five identities each identification links to, and the risk each one carries.
  </Card>

  <Card title="Identifiers" icon="fingerprint" href="/features/identification">
    User HID, Device ID, Visitor ID, and the per-call request ID, Session ID and Cookie ID.
  </Card>
</CardGroup>


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