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

# Server API

> Read any identification, or every identification of one account, device, visitor or public IP, from your backend.

The ShieldLabs Server API is how your backend reads results. The History API returns identifications: one by its request ID, or every identification of one user, device, visitor, public IP, session or cookie. ShieldLabs scores each identification asynchronously: the [JS snippet](/setup/snippet) collects signals, ShieldLabs scores them in about 300 ms, and the result arrives by [webhook](/api/webhooks) and is stored for this API to read.

Two backend APIs, both **server-side only**. Never call them from the browser:

For maintained language clients, see [SDKs](/api/sdks). The downloadable
[OpenAPI bundle](/references/openapi.yaml) is pinned to the public API description in
[shieldlabs-openapi](https://github.com/ShieldLabs-ai/shieldlabs-openapi), rather than a
separately maintained schema.

* **History API** (recommended) on `account.shieldlabs.ai`. Read identifications by request ID or by the user, device, visitor, public IP, session or cookie they belong to. **Free**: reads never use your included identifications. Response envelope `{ data, total }` (snake\_case).
* **Management API** on `api.shieldlabs.ai`. Read your profile: the remaining included volume on your account and your masked keys. For history, use the History API.

The History API accepts seven search types: `user_hid` (your account), `device_id`, `visitor_id`, `ip` (public IP), `request_id`, `session_id` and `cookie_id`.

Register webhook endpoints in the analytics dashboard under **Integration > Webhooks** (up to 10 per domain); the [setup guide](/setup/webhooks) walks through it.

### See also

* Collect signals in the browser: [JS snippet](/setup/snippet).
* Receive each identification as it is scored: [Webhooks](/api/webhooks).
* How signals become a Risk Score: [Identification Flow](/api/identification-flow).
* How identifications link to users, devices, visitors and IPs: [Users, devices, visitors and IPs](/concepts/entities).

## Base URLs

Paths start at the host: join the base URL and the path as they are written below.

| API | Base URL | Paths |
| - | - | - |
| **History API** | `https://account.shieldlabs.ai` | `/api/v1/history/{search_type}/{value}` |
| **Management API** | `https://api.shieldlabs.ai` | `/v1/profile` |

For development and staging, register a separate domain (for example `dev.example.com`) and call the same hosts with that domain's keys. The [environments guide](/setup/environments) walks through it.

## Authentication

Each API uses a different backend credential. Both are server-side only; an unauthenticated request returns `401`.

| API | Header(s) | Credential |
| - | - | - |
| **History API** | `Authorization: Bearer sec_…` | [Private API Key](/setup/keys). The domain is inferred from the key |
| **Management API** | `Authorization: Bearer <secret>` + `X-Shield-Domain: <domain>` | [Secret Key](/setup/keys) (hex) |

```
Authorization: Bearer sec_xxxxxxxx-xxxxxxxx-xxxxxxxx
```

<Warning>
  All server credentials must stay on your backend. Never put a Private API Key or Secret Key in the browser, the JS snippet, client logs, or a public repository. Webhook endpoints use separate `whsec_…` signing secrets. The browser-safe credential is the [Public Key](/setup/keys), which goes in the snippet, not here.
</Warning>

## Trying it out

The fastest check: read one scored identification back by its `request_id` with your Private API Key.

```bash theme={null}
curl "https://account.shieldlabs.ai/api/v1/history/request_id/550e8400-e29b-41d4-a716-446655440000?limit=1" \
  -H "Authorization: Bearer sec_your_private_api_key"
```

A `200` with a `data` array (empty is valid) means your key and host are correct. Full endpoint detail follows.

***

## History API (recommended)

Read stored identifications from your backend with the **Private API Key** ([authentication](#authentication) above). Each key reads the domain it belongs to. Wrong or missing credentials return `401` with a JSON body like `{"error":"invalid api key"}`.

### Endpoints at a glance

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/api/v1/history/{search_type}/{value}` | Search identifications by the account, device, visitor, public IP, session, cookie or request they belong to (the standard read path) |

For a single identification, query `history/request_id/{value}` with `limit=1` (shown below). For an account, use the recipe that follows.

### Read every identification of one account

Your users are the accounts you pass as a hashed User HID with `checkAuthenticatedUser`. To see how risky an account has been, read its identifications by `user_hid`, newest first, and page with `offset` while `offset` is below `total`.

```bash theme={null}
curl "https://account.shieldlabs.ai/api/v1/history/user_hid/e3b0c44298fc1c149afbf4c8996fb924?limit=100&offset=0" \
  -H "Authorization: Bearer sec_your_private_api_key"
```

```js theme={null}
const HISTORY_BASE = process.env.SHIELDLABS_API_URL ?? 'https://account.shieldlabs.ai'; // host only; paths start with /api/v1/
const NIL_DEVICE = '00000000-0000-0000-0000-000000000000';
const band = (s) => (s >= 60 ? 'Dangerous' : s >= 30 ? 'Suspicious' : 'Trusted');

async function readAccount(userHid, maxRows = 500) {
  const rows = [];
  for (let offset = 0; offset < maxRows; offset += 100) {
    const res = await fetch(
      `${HISTORY_BASE}/api/v1/history/user_hid/${encodeURIComponent(userHid)}?limit=100&offset=${offset}`,
      { headers: { Authorization: `Bearer ${process.env.SHIELDLABS_API_KEY}` } },
    );
    if (!res.ok) throw new Error(`History API ${res.status}`);
    const { data, total } = await res.json();
    rows.push(...data);
    if (data.length === 0 || offset + data.length >= total) break;
  }
  const scored = rows.filter((r) => r.score <= 100); // 999 marks a rate-limited identification
  const worst = scored.reduce((max, r) => Math.max(max, r.score), 0);
  return {
    identifications: scored.length,
    worstBand: scored.length ? band(worst) : null,
    devices: new Set(scored.map((r) => r.device_id).filter((d) => d && d !== NIL_DEVICE)),
    visitors: new Set(scored.map((r) => r.visitor_id).filter(Boolean)),
    publicIps: new Set(scored.map((r) => r.ip).filter(Boolean)),
    countries: new Set(scored.map((r) => r.country).filter(Boolean)),
  };
}
```

This is the paging form of the `accountView` helper in the [shared tutorial helpers](/use-case#the-shared-helpers), which reads the newest 100 identifications.

The worst band across the account's identifications is the account's risk, and the distinct Device IDs, Visitor IDs and public IPs are what the account is linked to. An all-zero Device ID (`00000000-0000-0000-0000-000000000000`) means no usable device signals reached ShieldLabs for that identification; the rate-limit marker (Risk Score 999) is one such case. Route it to review rather than allowing it; the recipe never counts it as a device. Read the same way by `device_id`, `visitor_id` or `ip` to see which accounts share a device, a visitor or an IP (skip rows whose `user_hid` is `anonymous`). Each call counts toward the soft limit of 15 requests per second per domain and never uses your included identifications. High-Risk Events on the account are available in the analytics dashboard, the API and webhooks. When one arrives for a user, act on the account; the Risk Score and risk signals of the identification remain the input at signup, login, checkout or withdrawal.

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

### GET `/api/v1/history/{search_type}/{value}`

Searches the identifications ShieldLabs has stored for your domain. Returns a paginated envelope, **newest first**. This is the guaranteed read path when a webhook may have been missed.

```bash theme={null}
curl "https://account.shieldlabs.ai/api/v1/history/request_id/550e8400-e29b-41d4-a716-446655440000?limit=1" \
  -H "Authorization: Bearer sec_your_private_api_key"
```

#### Path parameters

<ParamField path="search_type" type="string" required>
  The field to search on. One of:

  * `user_hid`: the hashed User HID of one of your accounts (free-form string)
  * `device_id`: a Device ID (UUID)
  * `visitor_id`: a Visitor ID (UUID)
  * `ip`: a public IP address (IPv4)
  * `request_id`: the request ID of one identification (UUID)
  * `session_id`: a Session ID (UUID), the browsing session
  * `cookie_id`: a Cookie ID (UUID)

  Always send one of the seven types above: an unknown type is not validated and returns the domain's latest rows unfiltered. Local IPs have no search type; keep `local_ip` from each webhook to group by it.
</ParamField>

<ParamField path="value" type="string" required>
  The value to match for the chosen `search_type`.
</ParamField>

#### Query parameters

<ParamField query="limit" type="integer" default="20">
  Maximum number of rows to return. Must be between **1** and **100**; values outside that range fall back to **20**. Rows are ordered newest first.
</ParamField>

<ParamField query="offset" type="integer" default="0">
  Number of rows to skip for pagination. Page while `offset` is below `total`.
</ParamField>

#### Response

```json theme={null}
{
  "data": [
    {
      "request_id":  "550e8400-e29b-41d4-a716-446655440000",
      "session_id":  "7a1b2c3d-4e5f-6789-abcd-ef0123456789",
      "cookie_id":   "3f2e1d0c-9b8a-7654-3210-fedcba987654",
      "device_id":   "d290f1ee-6c54-4b01-90e6-d701748f0851",
      "visitor_id":  "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "user_hid":    "e3b0c44298fc1c149afbf4c8996fb924",
      "domain":      "example.com",
      "ip":          "203.0.113.10",
      "country":     "US",
      "os":          "Windows",
      "browser":     "Chrome",
      "device_type": "desktop",
      "connection_type": "vpn",
      "score":            15,
      "score_details": "[{\"Value\":15,\"Description\":\"Is VPN\"}]",
      "is_vpn":      true,
      "created_at":  "2026-06-16 10:00:00"
    }
  ],
  "total": 1
}
```

The `data` array holds identifications in **snake\_case**. The `score_details` field is a JSON **string**; parse it to get the signal list. Each entry carries a numeric `Value` (the weight) and a free-text `Description`; branch on `Value` and the row `score`, not on the `Description` text, which is written for people and can change. Skip entries whose `Value` is 0: they are diagnostic notes, not risk signals. Field names map to the webhook body (`request_id` to `request_id`, `score` to `risk_score`, parsed `score_details` to `signals`). The example shows the core fields; a full row also carries the connection, network, traffic-attribution and per-signal flag columns:

* **Flags** use History names such as `is_vpn`, `is_proxy`, `is_datacenter`, `is_antidetect`, `is_js_disabled`, `is_browser_automation`, `is_search_bot` and `check_incomplete`. `browser_vpn_proxy` and `ip_mismatch` are on the webhook only.
* **Traffic attribution** is flat on the row: `traffic_channel`, `referrer_domain`, `entry_url`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term` and `click_id_type`. The webhook nests the same values under `traffic_source`, where `channel` is `traffic_channel` and `landing_url` is `entry_url`.

History reads never use your included identifications and never return `402`.

#### Common search patterns

<CodeGroup>
  ```bash Every identification of one account theme={null}
  curl "https://account.shieldlabs.ai/api/v1/history/user_hid/e3b0c44298fc1c149afbf4c8996fb924?limit=100" \
    -H "Authorization: Bearer sec_your_private_api_key"
  ```

  ```bash Every identification of a device theme={null}
  curl "https://account.shieldlabs.ai/api/v1/history/device_id/d290f1ee-6c54-4b01-90e6-d701748f0851?limit=100" \
    -H "Authorization: Bearer sec_your_private_api_key"
  ```

  ```bash Activity from a public IP theme={null}
  curl "https://account.shieldlabs.ai/api/v1/history/ip/203.0.113.10?limit=20" \
    -H "Authorization: Bearer sec_your_private_api_key"
  ```

  ```bash Read one identification theme={null}
  # Guaranteed read when a webhook may have been dropped.
  curl "https://account.shieldlabs.ai/api/v1/history/request_id/550e8400-e29b-41d4-a716-446655440000?limit=1" \
    -H "Authorization: Bearer sec_your_private_api_key"
  ```
</CodeGroup>

<Tip>
  Query by `request_id` with `limit=1` to read one identification. Query by `user_hid` to read an account before a sensitive action or during a review, and page with `offset` while `offset` is below `total`. Reads never use your included identifications; the History API accepts up to 15 requests per second per domain.
</Tip>

***

## Management API (`api.shieldlabs.ai`)

The Management API runs on `api.shieldlabs.ai`. It returns your **profile**: the domain, the remaining included volume on your account and your masked keys. For identification history, use the [History API](#history-api-recommended).

### Authentication

Credentials in headers, not in the URL:

```bash theme={null}
-H "X-Shield-Domain: myshop.com" \
-H "Authorization: Bearer YOUR_SECRET_KEY"
```

Wrong credentials, an unknown domain, or a disabled domain return `401` with an empty body.

### Endpoints at a glance

| Method | Path | Auth | Purpose | Cost |
| - | - | - | - | - |
| `GET` | `/v1/profile` | Headers | Domain, remaining included volume on your account, masked keys | Free |
| `GET` | `/v1/history/{type}/{value}?limit=N` | Headers | **Deprecated** identification search. Use the History API. | Free |

### GET `/v1/profile`

Returns the domain, the remaining included volume on your account and your masked keys. Keys are masked to their last four characters. This call is **free**.

```bash theme={null}
curl "https://api.shieldlabs.ai/v1/profile" \
  -H "X-Shield-Domain: myshop.com" \
  -H "Authorization: Bearer YOUR_SECRET_KEY"
```

#### Response

```json theme={null}
{
  "Domain":     "myshop.com",
  "Weight":     148230,
  "Callback":   "",
  "PublicKey":  "•••• a3f8",
  "Secret":     "•••• 9c2d",
  "CreatedAt":  "2026-01-15T09:00:00Z"
}
```

<ResponseField name="Domain" type="string">
  The registered domain this profile belongs to.
</ResponseField>

<ResponseField name="Weight" type="integer">
  The remaining included volume on your account, in identifications, shared by all your domains. Each identification uses 1. When your account's included volume is used up, the identification request returns HTTP `402` until the billing cycle resets or you change plan; the [Billing](/billing) page has the details. History and profile reads never use it.
</ResponseField>

<ResponseField name="Callback" type="string">
  Legacy field kept for older integrations. Webhook delivery uses the endpoints you register in the analytics dashboard under **Integration > Webhooks**.
</ResponseField>

<ResponseField name="PublicKey" type="string">
  Your Public Key, masked to the last four characters. The browser-safe credential that goes in the snippet URL. Read the full value in the analytics dashboard under **Integration > API keys**.
</ResponseField>

<ResponseField name="Secret" type="string">
  Your Secret Key, masked to the last four characters. Used for this API only. Copy it or replace it with **Rotate** in the analytics dashboard under **Integration > API keys**.
</ResponseField>

<ResponseField name="CreatedAt" type="string">
  ISO 8601 UTC timestamp of when the domain was created.
</ResponseField>

### GET `/v1/history/{type}/{value}`

<Warning>
  **Deprecated** (Sunset **2027-01-01**). Do not use this path for new integrations. Read identifications from the [History API](#history-api-recommended) on `account.shieldlabs.ai` (`GET https://account.shieldlabs.ai/api/v1/history/{search_type}/{value}`, `{ data, total }`, Private API Key). This endpoint remains live until sunset and returns `Deprecation`, `Sunset`, and `Link: rel="successor-version"` pointing at that History API URL.
</Warning>

Returns a **JSON array** of snapshot objects in **PascalCase**, newest first. Lookup types match the History API (seven identifiers). The call never uses your included identifications and never returns `402`.

```bash theme={null}
curl "https://api.shieldlabs.ai/v1/history/request_id/550e8400-e29b-41d4-a716-446655440000?limit=1" \
  -H "X-Shield-Domain: myshop.com" \
  -H "Authorization: Bearer YOUR_SECRET_KEY"
```

#### Path parameters

<ParamField path="type" type="string" required>
  The field to search on. One of (same set as the History API):

  * `user_hid`: the hashed User HID of one of your accounts (free-form string)
  * `device_id`: a Device ID (UUID validated)
  * `visitor_id`: a Visitor ID (UUID validated)
  * `ip`: a public IP address (IPv4 validated)
  * `request_id`: the request ID of one identification (UUID validated)
  * `session_id`: a Session ID (UUID validated)
  * `cookie_id`: a Cookie ID (UUID validated)

  Any other value returns `404`. A value in the wrong format (for example a non-UUID for `device_id`) returns `400`.
</ParamField>

<ParamField path="value" type="string" required>
  The value to match for the chosen `type`. UUID types are UUID validated, `ip` is IPv4 validated, `user_hid` is a free string.
</ParamField>

#### Query parameters

<ParamField query="limit" type="integer" default="100">
  Maximum number of rows to return. Capped at **100**: a higher value is clamped to 100. Rows are ordered newest first.
</ParamField>

***

## The Snapshot object (deprecated Management History)

On `api.shieldlabs.ai`, the deprecated History path returns identity and score fields in **PascalCase**, plus network columns captured during scoring. It carries no traffic source or detection flags. New work should parse the History API snake\_case envelope instead.

```json theme={null}
{
  "RequestID":            "550e8400-e29b-41d4-a716-446655440000",
  "SessionID":            "7a1b2c3d-4e5f-6789-abcd-ef0123456789",
  "CookieID":             "3f2e1d0c-9b8a-7654-3210-fedcba987654",
  "DeviceID":             "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "VisitorID":            "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "IP":                   "203.0.113.10",
  "OS":                   "Windows",
  "Browser":              "Chrome",
  "DeviceType":           "desktop",
  "Country":              "US",
  "UserHID":              "e3b0c44298fc1c149afbf4c8996fb924",
  "ConnectionType":       "vpn",
  "TcpMss":               1460,
  "MtuValue":             1500,
  "MtuHint":              "ethernet",
  "WebRtcHIP":            "203.0.113.10",
  "WebRtcCountry":        "US",
  "WebRtcConnectionType": "direct",
  "Score":                15,
  "Details": [
    { "Value": 15, "Description": "Is VPN" }
  ],
  "LastRequestTime":      "2026-06-16T10:00:00Z"
}
```

The identity and score fields map across three surfaces. Names differ; do not assume one JSON shape:

| Webhook (`data`) | History `account.shieldlabs.ai` | Management snapshot |
| - | - | - |
| `request_id` | `request_id` | `RequestID` |
| `user_hid` | `user_hid` | `UserHID` |
| `device_id` | `device_id` | `DeviceID` |
| `visitor_id` | `visitor_id` | `VisitorID` |
| `public_ip` | `ip`, `country` | `IP`, `Country` |
| `risk_score` | `score` | `Score` |
| `signals[{ name, weight }]` | parsed `score_details` | `Details[{ Value, Description }]` |
| `connection_type` | `connection_type` | `ConnectionType` |

See [Snapshot](/api/models#snapshot).

<ResponseField name="Browser" type="string">
  Detected browser name, for example `Chrome` or `Safari`.
</ResponseField>

<ResponseField name="DeviceType" type="string">
  Form factor: `desktop`, `mobile`, or `tablet`.
</ResponseField>

<ResponseField name="ConnectionType" type="string">
  The classified connection type, one of `direct`, `mobile`, `vpn`, `proxy`, `tor`, `privacy_relay`, `browser_vpn_proxy`, or `unknown` (when the type could not be resolved).
</ResponseField>

A snapshot also carries optional **network** fields below. They describe the connection. Keep them on your server.

<ResponseField name="TcpMss" type="integer">
  Optional network attribute on the snapshot. Keep it server-side.
</ResponseField>

<ResponseField name="MtuValue" type="integer">
  Optional network attribute on the snapshot. Keep it server-side.
</ResponseField>

<ResponseField name="MtuHint" type="string">
  Optional network attribute on the snapshot (a short label). Keep it server-side.
</ResponseField>

<ResponseField name="WebRtcHIP" type="string">
  Optional local IP field on the snapshot. Keep it server-side and do not display it to end users.
</ResponseField>

<ResponseField name="WebRtcCountry" type="string">
  Country associated with that local IP, when present.
</ResponseField>

<ResponseField name="WebRtcConnectionType" type="string">
  Connection type associated with that local IP, when present (for example `direct`).
</ResponseField>

<Note>
  A snapshot is a point-in-time record of one identification. The stored snapshot reflects the final Risk Score delivered on the webhook.
</Note>

## Reading the result

The Risk Score of one identification is a number from 0 to 100 that falls into three [Risk Score bands](/features/risk-scoring): Trusted 0-29, Suspicious 30-59, Dangerous 60-100. The row carries the number (`score`), so map it to a band in your backend, and treat any value above 100 as the 999 rate-limit marker. A user, device, visitor or IP takes the worst band of its identifications, which the [account read](#read-every-identification-of-one-account) above computes. Read a high Risk Score together with its named risk signals (`score_details`, or `signals` on the webhook) 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) has starting policies and worked examples.

## Errors

Error bodies are not uniform across surfaces, so branch on the HTTP status code first.

### History API (`account.shieldlabs.ai`)

| Status | Meaning | What to do |
| - | - | - |
| `200` | Success | Parse `data` and `total`. An empty `data` array is a valid result. |
| `401` | Missing, malformed or unknown Private API Key, or a disabled domain. Body `{"error":"missing or invalid authorization header"}` or `{"error":"invalid api key"}`. | Check `Authorization: Bearer sec_…` and that the domain is enabled. |
| `429` | More than 15 requests per second for this domain. Body `{"error":"too many requests"}`. | Back off for a second and retry. See [Rate limits](/rate-limits). |
| `500` | Internal error | Transient. Retry with backoff. |

### Management API (`api.shieldlabs.ai`)

| Status | Meaning | What to do |
| - | - | - |
| `200` | Success | Parse the response. Profile is a JSON object. A deprecated History search with a valid `{type}` that matches nothing returns `200` with `[]`. |
| `400` | Bad parameters | A malformed value, for example a non-UUID where a UUID is required. Fix the request. |
| `401` | Bad credentials or disabled domain | Check `X-Shield-Domain`, `Authorization: Bearer`, and that the domain is enabled. |
| `404` | Unsupported history type | `{type}` must be one of `user_hid`, `device_id`, `visitor_id`, `ip`, `request_id`, `session_id`, `cookie_id`. |
| `429` | Rate limit exceeded | Per-IP limit (15 requests per minute, then a 10-minute ban). Back off; see [Rate limits](/rate-limits). Body: `{"error":"too many requests"}`. |
| `503` | Gateway busy | Transient back-pressure. Retry with a short backoff. Body: `{"error":"server is busy"}`. |
| `500` | Internal error | Transient. Retry with backoff. |

On the Management API, `401` returns an empty body, and `400` and `404` return a bare JSON string. Only the identification request sent by the snippet returns `402`, when your account's included volume is used up; see [Billing](/billing).

The [Errors](/errors) page is the full reference across every surface.

## Next steps

<CardGroup cols={2}>
  <Card title="Data Models" icon="table-cells" href="/api/models">
    The full Snapshot, webhook body, and Score Detail schemas, and the identity each identifier maps to.
  </Card>

  <Card title="Webhooks" icon="bolt" href="/api/webhooks">
    The push delivery path: payload, `X-Shield-Signature` verification, and delivery guarantees.
  </Card>

  <Card title="Identification Flow" icon="diagram-project" href="/api/identification-flow">
    How an identification is scored and how the webhook and History API fit together.
  </Card>

  <Card title="API keys" icon="key" href="/setup/keys">
    Public Key, Private API Key, Secret Key: where each one belongs, and how to rotate.
  </Card>
</CardGroup>


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