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

> ShieldLabs Server API reference: read scored visitor-identification results from your backend with the History API, plus your domain profile and keys.

The ShieldLabs Server API is the **server-side** surface for your domain — the visitor identification and device fingerprinting API your backend calls to read scored results. ShieldLabs scores each visit asynchronously: the [JS snippet](/setup/snippet) collects signals, ShieldLabs scores them in about a second, 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:

* **History API** (recommended) on `account.shieldlabs.ai` — served by **Shield.Portal.Admin**. Read scored snapshots by identifier. **Free**: reads do not consume requests. Response envelope `{ data, total }` (snake\_case).
* **Management API** on `api.shieldlabs.ai` — served by **Shield.Core**. Read your domain profile and balance, plus an alternate history path that **bills per returned row** (PascalCase JSON array).

Both History surfaces accept the same lookup types: `ip`, `user_hid`, `visitor_id`, `request_id`, `device_id`, `session_id`, `cookie_id`.

Register webhook endpoints in the [dashboard](/setup/webhooks) **Webhooks** tab (up to 10 per domain).

### See also

* Collect signals in the browser: [JS snippet](/setup/snippet).
* Receive scores in real time: [Webhooks](/api/webhooks).
* How a signal becomes a score: [Identification Flow](/api/identification-flow).

## Base URLs

Use these hosts for the History API and Management API. The development hosts mirror production for local and staging work.

| Environment     | History API                             | Management API                                    |
| --------------- | --------------------------------------- | ------------------------------------------------- |
| **Production**  | `https://account.shieldlabs.ai/api`     | `https://api.shieldlabs.ai`                       |
| **Development** | `https://dev.account.shieldlabs.ai/api` | `https://api.shieldlabs.ai` (use dev-domain keys) |

## 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 snapshots from your backend with the **Private API Key** ([authentication](#authentication) above). 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 snapshots by identifier (the standard read path) |

For a single identification, query `history/request_id/{value}` with `limit=1` (shown below). Two extended routes — `/api/v1/request/{request_id}` and `/api/v1/debug/…` — return one snapshot or a search in an extended **PascalCase** shape with extra network and attribution fields, for investigations. They are not the standard integration path, and their response shape differs from the snake\_case `history` envelope below.

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

Searches the snapshots 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:

  * `ip`: client IP address (IPv4)
  * `user_hid`: the hashed user id you passed via the snippet (free-form string)
  * `visitor_id`: a VisitorID (UUID)
  * `request_id`: a single identification's RequestID (UUID)
  * `device_id`: a DeviceID (UUID)
  * `session_id`: a SessionID (UUID) — browsing session within a visit window
  * `cookie_id`: a CookieID (UUID)

  Always send one of the supported values above. An unsupported `{search_type}` is not validated and will not return the result you expect.
</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.
</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",
      "ip":          "203.0.113.10",
      "os":          "Windows",
      "browser":     "Chrome",
      "device_type": "desktop",
      "country":     "US",
      "user_hid":    "e3b0c44298fc1c149afbf4c8996fb924",
      "connection_type": "vpn",
      "score":            15,
      "score_details": "[{\"Value\":15,\"Description\":\"VPN\"}]",
      "created_at":  "2026-06-16 10:00:00"
    }
  ],
  "total": 1
}
```

The `data` array holds snapshot objects 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-form `Description`; branch on `Value` and the row `score`, not on the `Description` text, which is human-readable and not a stable contract. Field names map to the webhook body (`request_id` ↔ `request_id`, `score` ↔ `risk_score`, parsed `score_details` ↔ `signals`). The example shows the core fields; a full snapshot also carries the connection, network, traffic-attribution, and per-signal flag columns.

History reads through this API do **not** consume request balance and do not return `402`.

#### Common search patterns

<CodeGroup>
  ```bash Read one identification theme={null}
  # Guaranteed-read fallback 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"
  ```

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

  ```bash History for one of your users theme={null}
  curl "https://account.shieldlabs.ai/api/v1/history/user_hid/e3b0c44298fc1c149afbf4c8996fb924?limit=50" \
    -H "Authorization: Bearer sec_your_private_api_key"
  ```

  ```bash Activity from an 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"
  ```
</CodeGroup>

<Tip>
  Query by `request_id` with `limit=1` when you only need one scored result. Reserve wider `device_id` / `user_hid` / `ip` searches for investigations.
</Tip>

***

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

The Management API runs on `api.shieldlabs.ai`. It serves **profile and balance**, and an alternate History path that bills against your domain balance.

### 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/history/{type}/{value}?limit=N` | Headers | Search snapshots (alternate History path) | 1 request per returned row (minimum 1) |
| `GET`  | `/v1/profile`                        | Headers | Domain config, balance, masked keys       | Free (0 requests)                      |

### GET `/v1/profile`

Returns your domain's configuration and current balance. 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,
  "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">
  Your remaining balance, measured in requests. One identification consumes 1 request, and the Management History API consumes 1 request per returned row. When this reaches 0, scoring and Management History calls return `402`, as the [Billing](/billing) page details.
</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 from the dashboard.
</ResponseField>

<ResponseField name="Secret" type="string">
  Your Secret Key, masked to the last four characters. Used for this API only. The full value is shown only at creation in the dashboard.
</ResponseField>

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

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

Alternate History path on the Management API. Returns a **JSON array** of snapshot objects in **PascalCase**, newest first. Prefer the [History API](#history-api-recommended) for new work.

```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):

  * `ip`: client IP address (IPv4 validated)
  * `user_hid`: the hashed user id you passed via the snippet (free-form string)
  * `visitor_id`: a VisitorID (UUID validated)
  * `request_id`: a single identification's RequestID (UUID validated)
  * `device_id`: a DeviceID (UUID validated)
  * `session_id`: a SessionID (UUID validated)
  * `cookie_id`: a CookieID (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>

#### Billing

The Management History API bills **1 request per returned row**. An empty result still bills **1 request** (the lookup itself). If your balance is insufficient for the result, the call returns `402`.

<Note>
  The 1-request lookup charge is taken before the lookup is validated, so a `400` (malformed value) or `404` (unsupported `{type}`) still bills 1 request. Only `401` (bad credentials) and `402` (out of requests) cost 0.
</Note>

***

## The Snapshot object (Management API)

On `api.shieldlabs.ai`, History returns a **superset of the webhook body**: identity and score fields in **PascalCase**, plus connection and network detail captured during scoring.

```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": "VPN" }
  ],
  "LastRequestTime":      "2026-06-16T10:00:00Z"
}
```

The identity and score fields map to the webhook body (`request_id`, `risk_score`, `signals[{ name, weight }]`, …). Note the field name differs by surface: this PascalCase Management snapshot uses `Score`, the recommended `account.shieldlabs.ai` History rows use `score`, and the webhook `data` uses `risk_score`. See [Snapshot](/api/models#snapshot).

<ResponseField name="Browser" type="string">
  Detected browser name, e.g. `Chrome`, `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 the **Network Intelligence** fields below. They describe the connection itself, are captured during scoring, and are useful for your own forensics and correlation.

<ResponseField name="TcpMss" type="integer">
  The TCP maximum segment size observed for the connection, a low-level network attribute captured during the network check.
</ResponseField>

<ResponseField name="MtuValue" type="integer">
  The network MTU (maximum transmission unit) derived from the connection.
</ResponseField>

<ResponseField name="MtuHint" type="string">
  A short label for the link type inferred from the MTU, for example `ethernet`.
</ResponseField>

<ResponseField name="WebRtcHIP" type="string">
  The visitor's local (host) IP observed during the network check. This is a **server-side correlation field** for the local-IP entity: keep it server-side and do not display it to end users.
</ResponseField>

<ResponseField name="WebRtcCountry" type="string">
  Country derived from the network-check IP, used to cross-check against the public-IP `Country`.
</ResponseField>

<ResponseField name="WebRtcConnectionType" type="string">
  Connection type classified from the network check, for example `direct`.
</ResponseField>

<Note>
  A snapshot is a point-in-time record of one identification. When a follow-up network check completes after the first score, the stored snapshot reflects the recomputed value (the webhook itself is sent once, after that follow-up window).
</Note>

## Reading the score

ShieldLabs scores. **Your application decides.** The Risk Score is a 0 to 100 number that falls into four [Risk Score bands](/features/risk-scoring), and your code 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 behind a corporate proxy, a VPN, or a privacy browser. Tune your thresholds gradually, working from the [per-band playbook](/guides/acting-on-risk-score) and its worked examples.

## Errors

Error bodies are not uniform across surfaces — 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`  | Bad or missing Private API Key | Check `Authorization: Bearer sec_…` and that the domain is enabled. |
| `500`  | Internal error                 | Transient. Retry with backoff.                                      |

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

| Status | Meaning                            | What to do                                                                                                                         |
| ------ | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `200`  | Success                            | Parse the response. A History search with a valid `{type}` that matches nothing returns `200` with `[]` and still bills 1 request. |
| `400`  | Bad parameters                     | A malformed value, e.g. 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.                                                  |
| `402`  | Out of requests                    | Balance exhausted (Management History needs 1 request per returned row). Top up from the [Billing](/billing) page.                 |
| `404`  | Unsupported history type           | `{type}` must be one of `ip`, `user_hid`, `visitor_id`, `request_id`, `device_id`.                                                 |
| `429`  | Rate limit exceeded                | Per-IP limit (20 req/min, 1-hour 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` and `402` return an empty body. `400` and `404` return a bare JSON string.

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 in one place.
  </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 signals become a score and how the webhook and History API fit together.
  </Card>

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