> ## 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

> Learn how a visit becomes a scored result and how the webhook and History API fit together.

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.

<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, query the [History API](/api/server-api).
</Info>

## The three steps

<Steps>
  <Step title="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.
  </Step>

  <Step title="Server scores asynchronously (~1s)">
    The server enriches the signals (IP intelligence and network analysis) and computes the [Risk Score](/features/risk-scoring) with an explainable `Details` array.
  </Step>

  <Step title="Score is delivered (webhook + History API)">
    The server pushes **one** final [webhook](/api/webhooks) per check (within up to 60 seconds, after optional follow-up network checks). You can also read the result any time from the [History API](/api/server-api) by `request_id`.
  </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 /snapshot/{requestID}?publicKey=...
    R-->>B: 200 OK  "203.0.113.10"  (ack, not the score)
    Note over S: enrich + score + optional follow-ups (≤60s)
    S->>Y: webhook  flat payload  (score + signals, once)
    Y->>A: GET /api/v1/history/request_id/{requestID}  (guaranteed read)
```

## 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.

```http theme={null}
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](/setup/keys). 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:**

```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 billed. It does **not** contain the VisitorID, DeviceID, or Risk Score. Those are computed server-side and delivered in Step 3.

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

In the browser, the snippet surfaces the same acknowledgment and the `requestID` through its optional callback, so you can correlate client and server records:

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

mod.checkAnonymous(undefined, (ip, requestID) => {
  // ip = the client IP from the snapshot acknowledgment, 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 }),
  });
});
```

The [snippet install guide](/setup/snippet) has the full method list 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:

<CardGroup cols={3}>
  <Card title="IP intelligence" icon="globe">
    The server looks up reputation and connection type for the client IP (VPN, proxy, datacenter, privacy relay).
  </Card>

  <Card title="Network analysis" icon="network-wired">
    Network-level attributes are analyzed and compared against what the browser claims (this is what powers OS-mismatch and VPN corroboration).
  </Card>

  <Card title="Follow-up network check" icon="signal">
    A follow-up network check completes shortly after the initial POST and can refine the score.
  </Card>
</CardGroup>

Because these run concurrently and follow-up checks resolve slightly later, ShieldLabs waits for the optional follow-up network-check evidence, then delivers **one** final webhook per check (within **60 seconds**).

The [VPN corroboration](/features/anonymity-signals) behind a VPN signal 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 `POST`s the score to each enabled webhook endpoint **once per check**, joined by `request_id`:

| Timing                                                   | Content                                                            |
| -------------------------------------------------------- | ------------------------------------------------------------------ |
| \~1s (simple visits)                                     | Full `risk_score` and `signals`                                    |
| Up to **60s** when a follow-up network check is expected | Final score after follow-ups finish, or best available at deadline |

The webhook body is a signed snake\_case **envelope**. The signature travels in the `X-Shield-Signature` header:

```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",
    "risk_score": 35,
    "signals": [
      { "name": "vpn", "weight": 15 },
      { "name": "datacenter_ip", "weight": 10 }
    ],
    "observed_at": "2026-06-16T10:00:45Z"
  }
}
```

Full field schema: [WebhookEvent](/api/models#webhookevent).

<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).
</Note>

Webhook delivery is **at-most-once with no retries** (a \~1 second 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, signature verification in Node/Go/Python, and delivery guarantees are in [Webhooks](/api/webhooks).

### History API (read)

You can read the scored result for any `RequestID` 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 with snapshots (newest first). You can search by `request_id`, `visitor_id`, `device_id`, `user_hid`, `ip`, `session_id`, or `cookie_id`. History reads through `account.shieldlabs.ai` do not bill per row. The full schema and alternate Management API path are in the [Server API](/api/server-api) reference.

## The RequestID lifecycle

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

<CardGroup cols={3}>
  <Card title="Snapshot" icon="upload">
    Minted client-side as a UUID and sent in the POST path: `/snapshot/{requestID}`. Returned to your page in the snippet callback.
  </Card>

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

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

Persist the `requestID` early, record `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 never does (allow up to \~60s). The full reliability pattern, with signature verification, is in [Webhooks](/api/webhooks#delivery-guarantees).

## What you do with the score

ShieldLabs scores. Your code decides. The Risk Score lands in the 0–100 range and falls into four [Risk Score bands](/features/risk-scoring) you can act on, with your application 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). The [per-band playbook](/guides/acting-on-risk-score) gives threshold guidance 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 snapshot schema, profile, and billing.
  </Card>

  <Card title="Risk Score" icon="gauge" href="/features/risk-scoring">
    How the 0–100 explainable score and the Clean / Low / Medium / High bands work.
  </Card>

  <Card title="Identifiers" icon="fingerprint" href="/features/identification">
    RequestID, SessionID, CookieID, DeviceID, VisitorID, and UserHID mechanics.
  </Card>
</CardGroup>
