---
title: "Device intelligence API"
description: "Read devices, events and visits, export data, fetch the sealed-results key, write labels, and manage behavioral baselines."
canonical_url: "https://noxtica.com/docs/api/reference/device/"
markdown_url: "https://noxtica.com/docs/api/reference/device/index.md"
last_updated: 2026-10-27
published: 2026-10-27
section: "server-apis"
section_title: "Server APIs"
openapi_release: "v0.4.0"
openapi_spec_version: "1.3.0-2026-10-27"
site: "Noxtica"
language: "en"
source: markdown
---

# Device intelligence API

Read devices, events and visits, export data, fetch the sealed-results key, write labels, and manage behavioral baselines.

Server-to-server reads and writes over the devices, events and visits your integrations collected. Authenticate with an `sk_` server API key whose scopes cover the operation: `read:device`, `read:fingerprints`, `read:events`, `read:visits`, `read:export`, `read:sealed_results`, `write:labels`, `write:generate`, `read:behavioral` and `erase:behavioral`. Base URL: `https://collect.noxtica.com`.

The signed verdict webhooks (`submission.scored`, `risk.high`, `risk.flagged`, `risk.cluster_flagged`, `account.agent_linked`) are documented in this area too.

| Environment | Base URL |
| --- | --- |
| Production | `https://collect.noxtica.com` |

Credential: `ApiKeyAuth`. See [Authentication](https://noxtica.com/docs/api/authentication/).

## Look up a device by ID {#v1GetDevice}

`GET /v1/device/{deviceId}`

Returns the current score and risk projection of one device — the `fingerprintId` your browser or mobile integration received — scoped to the calling organisation.

The route answers `404` with `{"error":"Device not found","deviceId":"...","requestId":"..."}` when the device is unknown, belongs to another organisation, or has not been seen within your organisation's configured retention window.

- `reasonCodes` is the published, non-personal reason-code projection of the internal flags. It is an empty array unless reason-code output is enabled for your organisation, independently of the raw `flags` field.
- `gpuAttestationTier` is masked: a stored `hardware` or `software` tier is reported as `attested` unless publication of the hardware attestation level is enabled for your organisation. `gpuVerified` is not masked and never asserts `hardware`.
- `subscores` and `details` are the scorer's per-domain breakdown when present, otherwise `null`. `softwareSubfp`, `hardwareSubfp` and `environmentSubfp` are server-derived observational sub-fingerprints; they are `null` for devices recorded before those fields existed.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:device`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `deviceId` | path | string | Yes | The `device_id` to look up (URL-decoded from the path segment). |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The device's current score/risk projection. |  | `application/json` |
| `400` | Missing/empty `deviceId`, or the path segment failed URL-decoding. | `invalid_request` | `application/json` ErrorEnvelope |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `404` | `{ error: 'Device not found', deviceId, requestId }` — tenant projection inactive/missing OR the row is absent/outside the retention window. | `Device not found` | `application/json` |
| `429` | Rate limit exceeded. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying; the response carries no other rate-limit headers. | `Rate limit exceeded` | `application/json` |
| `500` | Unexpected server fault. Retry with backoff and quote `requestId` to support if it persists. | `Internal server error` | `application/json` ErrorEnvelope |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `device` | object | Yes |  |
| `device.deviceId` | string | No |  |
| `device.domainId` | string | No |  |
| `device.score` | number | No |  |
| `device.riskLevel` | string | No |  |
| `device.confidence` | number, nullable | No |  |
| `device.flags` | array of string | No |  |
| `device.reasonCodes` | array of string | No | Empty unless reason-code output is enabled for your organisation. |
| `device.subscores` | object, nullable | No |  |
| `device.details` | object, nullable | No |  |
| `device.country` | string, nullable | No |  |
| `device.city` | string, nullable | No |  |
| `device.createdAt` | string (date-time) | No |  |
| `device.updatedAt` | string (date-time) | No |  |
| `device.lastSubmittedAt` | string (date-time), nullable | No |  |
| `device.lastSeenAt` | string (date-time), nullable | No |  |
| `device.gpuVerified` | boolean | No |  |
| `device.gpuAttestationTier` | string, nullable | No | Masked unless publication of the hardware attestation level is enabled for your organisation; see description above. Allowed values: `attested`, `suspicious`, `unavailable`, `failed`, `hardware`, `software`. |
| `device.softwareSubfp` | string, nullable | No |  |
| `device.hardwareSubfp` | string, nullable | No |  |
| `device.environmentSubfp` | string, nullable | No |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "device": {
    "deviceId": "dev_7a2b9c4d1e6f3a8b5c2d9e4f7a1b6c3d",
    "domainId": "dom_3f2a8c91d6e5f470",
    "score": 1,
    "riskLevel": "low",
    "confidence": 1,
    "flags": [
      "example-flags"
    ],
    "reasonCodes": [
      "example-reasonCodes"
    ],
    "subscores": {},
    "details": {},
    "country": "US",
    "city": "Istanbul",
    "createdAt": "2026-09-13T12:00:00.000Z",
    "updatedAt": "2026-09-13T12:00:00.000Z",
    "lastSubmittedAt": "2026-09-13T12:00:00.000Z",
    "lastSeenAt": "2026-09-13T12:00:00.000Z",
    "gpuVerified": true,
    "gpuAttestationTier": "attested",
    "softwareSubfp": "example-softwareSubfp",
    "hardwareSubfp": "example-hardwareSubfp",
    "environmentSubfp": "example-environmentSubfp"
  }
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/device/example-deviceId" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/device/example-deviceId', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/device/example-deviceId",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## List devices {#v1ListDevices}

`GET /v1/devices`

Lists devices, most recently updated first, with filters and keyset pagination. `from` and `to` bound the range and may span at most 400 days. `source` selects real devices, synthetic devices (see the generator operations) or both; `all` merges both result sets by `(updated_at, device_id)` descending.

`cursor` is opaque: pass the `nextCursor` of the previous page back unchanged and never construct one. `limit` defaults to 50 and is capped at 200.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:fingerprints`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | No | Minimum 1, maximum 200. Default `50`. |
| `cursor` | query | string | No | Opaque keyset cursor from a prior response's `nextCursor`. |
| `from` | query | string (date-time) | No | Defaults to 400 days before now. |
| `to` | query | string (date-time) | No | Defaults to now. |
| `domain_id` | query | string | No |  |
| `risk_level` | query | string | No | Filter by risk level; the allowed set is deps-injected (`riskLevelFilterValues`). |
| `source` | query | string | No | Allowed values: `real`, `synthetic`, `all`. Default `real`. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | A page of devices. |  | `application/json` |
| `400` | `Invalid source`, `Invalid risk_level`, or `Invalid date range` (bad/inverted/over-400-day `from`/`to`). | `invalid_request` | `application/json` ErrorEnvelope |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `429` | Rate limit exceeded. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying; the response carries no other rate-limit headers. | `Rate limit exceeded` | `application/json` |
| `500` | `{ error: 'Failed to query devices', code: 'devices_list_query_failed' }` on a warehouse fault, or a generic internal error. | `invalid_request` | `application/json` ErrorEnvelope |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `devices` | array of object | Yes |  |
| `devices[]` | object | No |  |
| `devices[].deviceId` | string | No |  |
| `devices[].domainId` | string | No |  |
| `devices[].score` | number, nullable | No |  |
| `devices[].riskLevel` | string, nullable | No |  |
| `devices[].flags` | array of string | No |  |
| `devices[].country` | string, nullable | No |  |
| `devices[].createdAt` | string (date-time) | No |  |
| `devices[].updatedAt` | string (date-time) | No |  |
| `devices[].isSynthetic` | boolean | No |  |
| `hasMore` | boolean | Yes |  |
| `nextCursor` | string, nullable | Yes |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "devices": [
    {
      "deviceId": "dev_7a2b9c4d1e6f3a8b5c2d9e4f7a1b6c3d",
      "domainId": "dom_3f2a8c91d6e5f470",
      "score": 1,
      "riskLevel": "low",
      "flags": [
        "example-flags"
      ],
      "country": "US",
      "createdAt": "2026-09-13T12:00:00.000Z",
      "updatedAt": "2026-09-13T12:00:00.000Z",
      "isSynthetic": true
    }
  ],
  "hasMore": true,
  "nextCursor": "cursor_000123"
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/devices" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/devices', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/devices",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## List devices linked to an account {#v1ListAccountDevices}

`GET /v1/accounts/{accountHash}/devices`

Lists the devices and agents linked to one of your end-user accounts, with their current score and risk only. `accountHash` must be the keyed 16-character lowercase-hex pseudonym your organisation already receives elsewhere (for example the `account_hash` filter of `GET /v1/events`); a raw account id is never accepted on this path.

Pagination is a keyset cursor over `(last_seen_at, id)` descending, with the same opaque cursor shape as `GET /v1/events`.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:fingerprints`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `accountHash` | path | string | Yes | The keyed 16-hex-lowercase account pseudonym. Pattern `^[0-9a-f]{16}$`. |
| `limit` | query | integer | No | Minimum 1, maximum 200. Default `50`. |
| `cursor` | query | string | No |  |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The account's linked device/agent history. |  | `application/json` |
| `400` | `accountHash` fails the 16-hex-lowercase shape check. | `invalid_request` | `application/json` ErrorEnvelope |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `429` | Rate limit exceeded. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying; the response carries no other rate-limit headers. | `Rate limit exceeded` | `application/json` |
| `500` | `{ error: 'Failed to query account devices', code: 'account_devices_query_failed' }` or a generic internal error. | `invalid_request` | `application/json` ErrorEnvelope |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `accountHash` | string | Yes |  |
| `links` | array of object | Yes |  |
| `links[]` | object | No |  |
| `links[].agentKeyid` | string | No |  |
| `links[].agentHost` | string, nullable | No |  |
| `links[].deviceId` | string | No |  |
| `links[].domainId` | string | No |  |
| `links[].firstSeenAt` | integer | No | Epoch ms. |
| `links[].lastSeenAt` | integer | No | Epoch ms. |
| `links[].seenCount` | integer | No |  |
| `links[].deviceScore` | number, nullable | No |  |
| `links[].deviceRiskLevel` | string, nullable | No |  |
| `links[].deviceUpdatedAt` | string, nullable | No |  |
| `hasMore` | boolean | Yes |  |
| `nextCursor` | string, nullable | Yes |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "accountHash": "a1b2c3d4e5f60718293a4b5c6d7e8f901a2b3c4d5e6f708192a3b4c5d6e7f89",
  "links": [
    {
      "agentKeyid": "example-agentKeyid",
      "agentHost": "example-agentHost",
      "deviceId": "dev_7a2b9c4d1e6f3a8b5c2d9e4f7a1b6c3d",
      "domainId": "dom_3f2a8c91d6e5f470",
      "firstSeenAt": 1,
      "lastSeenAt": 1,
      "seenCount": 1,
      "deviceScore": 1,
      "deviceRiskLevel": "example-deviceRiskLevel",
      "deviceUpdatedAt": "example-deviceUpdatedAt"
    }
  ],
  "hasMore": true,
  "nextCursor": "cursor_000123"
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/accounts/example-accountHash/devices" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/accounts/example-accountHash/devices', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/accounts/example-accountHash/devices",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## List scoring events {#v1ListEvents}

`GET /v1/events`

Lists scored events, newest first, with a keyset cursor over `(created_at, id)`. The optional `account_hash` filter (a keyed 16-character lowercase-hex pseudonym) applies to `source=real` only: synthetic events and the merged `all` view carry no `account_hash`.

`reasonCodes` is the published reason-code projection of the event's internal flags; it is empty unless reason-code output is enabled for your organisation.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:events`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `device_id` | query | string | No |  |
| `from` | query | string (date-time) | No |  |
| `to` | query | string (date-time) | No |  |
| `domain_id` | query | string | No |  |
| `account_hash` | query | string | No | Applied only when `source` resolves to `real`. Pattern `^[0-9a-f]{16}$`. |
| `source` | query | string | No | Allowed values: `real`, `synthetic`, `all`. Default `real`. |
| `limit` | query | integer | No | Minimum 1, maximum 200. Default `50`. |
| `cursor` | query | string | No |  |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | A page of events. |  | `application/json` |
| `400` | `Invalid source`, or `account_hash` fails the 16-hex-lowercase shape check. | `invalid_request` | `application/json` ErrorEnvelope |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `429` | Rate limit exceeded. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying; the response carries no other rate-limit headers. | `Rate limit exceeded` | `application/json` |
| `500` | Unexpected server fault. Retry with backoff and quote `requestId` to support if it persists. | `Internal server error` | `application/json` ErrorEnvelope |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `events` | array of object | Yes |  |
| `events[]` | object | No |  |
| `events[].id` | string | No |  |
| `events[].deviceId` | string | No |  |
| `events[].domainId` | string | No |  |
| `events[].score` | number, nullable | No |  |
| `events[].riskLevel` | string, nullable | No |  |
| `events[].confidence` | number, nullable | No |  |
| `events[].flags` | array of string | No |  |
| `events[].reasonCodes` | array of string | No |  |
| `events[].country` | string, nullable | No |  |
| `events[].createdAt` | string (date-time) | No |  |
| `events[].payloadUnavailable` | boolean, nullable | No |  |
| `events[].isSynthetic` | boolean | No |  |
| `hasMore` | boolean | Yes |  |
| `nextCursor` | string, nullable | Yes |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "events": [
    {
      "id": "example-id",
      "deviceId": "dev_7a2b9c4d1e6f3a8b5c2d9e4f7a1b6c3d",
      "domainId": "dom_3f2a8c91d6e5f470",
      "score": 1,
      "riskLevel": "low",
      "confidence": 1,
      "flags": [
        "example-flags"
      ],
      "reasonCodes": [
        "example-reasonCodes"
      ],
      "country": "US",
      "createdAt": "2026-09-13T12:00:00.000Z",
      "payloadUnavailable": true,
      "isSynthetic": true
    }
  ],
  "hasMore": true,
  "nextCursor": "cursor_000123"
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/events" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/events', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/events",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## List visits {#v1ListVisits}

`GET /v1/visits`

Lists visit records, newest first, with a keyset cursor over `(created_at, id)`. Visits have no synthetic partition, so this route has no `source` filter. `meta` is the visit's own parsed JSON metadata and includes `external_user_id` when the submitting page set it.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:visits`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `device_id` | query | string | No |  |
| `from` | query | string (date-time) | No |  |
| `to` | query | string (date-time) | No |  |
| `domain_id` | query | string | No |  |
| `external_user_id` | query | string | No |  |
| `limit` | query | integer | No | Minimum 1, maximum 200. Default `50`. |
| `cursor` | query | string | No |  |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | A page of visits. |  | `application/json` |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `429` | Rate limit exceeded. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying; the response carries no other rate-limit headers. | `Rate limit exceeded` | `application/json` |
| `500` | Unexpected server fault. Retry with backoff and quote `requestId` to support if it persists. | `Internal server error` | `application/json` ErrorEnvelope |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `visits` | array of object | Yes |  |
| `visits[]` | object | No |  |
| `visits[].id` | string | No |  |
| `visits[].deviceId` | string | No |  |
| `visits[].domainId` | string | No |  |
| `visits[].country` | string, nullable | No |  |
| `visits[].city` | string, nullable | No |  |
| `visits[].meta` | object | No | The row's own parsed JSON `meta` blob (e.g. `external_user_id`); shape is submitter-controlled and not locally validated. |
| `visits[].createdAt` | string (date-time) | No |  |
| `hasMore` | boolean | Yes |  |
| `nextCursor` | string, nullable | Yes |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "visits": [
    {
      "id": "example-id",
      "deviceId": "dev_7a2b9c4d1e6f3a8b5c2d9e4f7a1b6c3d",
      "domainId": "dom_3f2a8c91d6e5f470",
      "country": "US",
      "city": "Istanbul",
      "meta": {},
      "createdAt": "2026-09-13T12:00:00.000Z"
    }
  ],
  "hasMore": true,
  "nextCursor": "cursor_000123"
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/visits" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/visits', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/visits",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Export fingerprints as CSV or JSON {#v1ExportFingerprints}

`GET /v1/exports/fingerprints`

Bulk-exports device fingerprint records for a date range. `from` and `to` are required and are validated for both shape and calendar (a malformed string or an impossible date such as `2026-02-30` is rejected) before being echoed into the CSV `Content-Disposition` filename.

Each request returns at most a fixed maximum number of rows. A capped page sets `truncated: true` (JSON) or the `X-Noxtica-Export-Truncated: true` header (CSV) and an opaque keyset `next_cursor` (JSON) or `X-Noxtica-Export-Next-Cursor` header (CSV) over `(created_at, id)` descending; request the next page with that cursor.

`reason_codes` is the published reason-code projection (empty unless reason-code output is enabled for your organisation) and is the last CSV column, so fixed-position consumers are unaffected.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:export`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `format` | query | string | No | Allowed values: `json`, `csv`. Default `json`. |
| `from` | query | string | Yes | REQUIRED. `YYYY-MM-DD`, `YYYY-MM-DD HH:MM:SS`, or ISO 8601 (optional ms/offset). |
| `to` | query | string | Yes |  |
| `domain_id` | query | string | No |  |
| `source` | query | string | No | Allowed values: `real`, `synthetic`, `all`. Default `real`. |
| `cursor` | query | string | No | Opaque keyset cursor from a prior response's `next_cursor`. Omit for page 1. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The exported page, as a JSON envelope or a raw CSV body depending on `format`. |  | `application/json`, `text/csv` |
| `400` | `Invalid source`/`Invalid format`, missing `from`/`to`, or `from and to must be YYYY-MM-DD or ISO 8601 datetime values`. | `invalid_request` | `application/json` ErrorEnvelope |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `429` | Rate limit exceeded. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying; the response carries no other rate-limit headers. | `Rate limit exceeded` | `application/json` |
| `500` | Unexpected server fault. Retry with backoff and quote `requestId` to support if it persists. | `Internal server error` | `application/json` ErrorEnvelope |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `200` | `X-Noxtica-Export-Next-Cursor` | string |  |
| `200` | `X-Noxtica-Export-Truncated` | string |  |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `count` | integer | Yes |  |
| `limit` | integer | Yes |  |
| `truncated` | boolean | Yes |  |
| `from` | string | Yes |  |
| `to` | string | Yes |  |
| `next_cursor` | string | No |  |
| `fingerprints` | array of object | Yes |  |
| `fingerprints[]` | object | No |  |
| `fingerprints[].device_id` | string | No |  |
| `fingerprints[].domain_id` | string | No |  |
| `fingerprints[].score` | number, nullable | No |  |
| `fingerprints[].risk_level` | string, nullable | No |  |
| `fingerprints[].flags` | string | No | Semicolon-joined. |
| `fingerprints[].reason_codes` | string | No | Semicolon-joined; empty unless reason-code output is enabled for your organisation. |
| `fingerprints[].country` | string | No |  |
| `fingerprints[].city` | string | No |  |
| `fingerprints[].created_at` | string (date-time) | No |  |
| `fingerprints[].updated_at` | string (date-time) | No |  |
| `fingerprints[].is_synthetic` | string | No | Allowed values: `true`, `false`. |

**Response example** (`200`)

```json
{
  "ok": true,
  "count": 1,
  "limit": 1,
  "truncated": true,
  "from": "example-from",
  "to": "example-to",
  "next_cursor": "cursor_000123",
  "fingerprints": [
    {
      "device_id": "dev_7a2b9c4d1e6f3a8b5c2d9e4f7a1b6c3d",
      "domain_id": "dom_3f2a8c91d6e5f470",
      "score": 1,
      "risk_level": "low",
      "flags": "example-flags",
      "reason_codes": "example-reason_codes",
      "country": "US",
      "city": "Istanbul",
      "created_at": "2026-09-13T12:00:00.000Z",
      "updated_at": "2026-09-13T12:00:00.000Z",
      "is_synthetic": "true"
    }
  ]
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/exports/fingerprints?from=example-from&to=example-to" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/exports/fingerprints?from=example-from&to=example-to', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/exports/fingerprints?from=example-from&to=example-to",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Export events as CSV or JSON {#v1ExportEvents}

`GET /v1/exports/events`

Bulk-exports scored events for a date range. The contract is identical to `GET /v1/exports/fingerprints`: the same date validation, per-request row cap, and `truncated` / `next_cursor` pagination.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:export`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `format` | query | string | No | Allowed values: `json`, `csv`. Default `json`. |
| `from` | query | string | Yes |  |
| `to` | query | string | Yes |  |
| `domain_id` | query | string | No |  |
| `source` | query | string | No | Allowed values: `real`, `synthetic`, `all`. Default `real`. |
| `cursor` | query | string | No |  |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The exported page, as a JSON envelope or a raw CSV body depending on `format`. |  | `application/json`, `text/csv` |
| `400` | `Invalid source`/`Invalid format`, missing `from`/`to`, or invalid date shape/calendar. | `invalid_request` | `application/json` ErrorEnvelope |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `429` | Rate limit exceeded. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying; the response carries no other rate-limit headers. | `Rate limit exceeded` | `application/json` |
| `500` | Unexpected server fault. Retry with backoff and quote `requestId` to support if it persists. | `Internal server error` | `application/json` ErrorEnvelope |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `200` | `X-Noxtica-Export-Next-Cursor` | string |  |
| `200` | `X-Noxtica-Export-Truncated` | string |  |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `count` | integer | Yes |  |
| `limit` | integer | Yes |  |
| `truncated` | boolean | Yes |  |
| `from` | string | Yes |  |
| `to` | string | Yes |  |
| `next_cursor` | string | No |  |
| `events` | array of object | Yes |  |
| `events[]` | object | No |  |
| `events[].id` | string | No |  |
| `events[].device_id` | string | No |  |
| `events[].domain_id` | string | No |  |
| `events[].score` | number, nullable | No |  |
| `events[].risk_level` | string, nullable | No |  |
| `events[].reason_codes` | string | No | Semicolon-joined; empty unless reason-code output is enabled for your organisation. |
| `events[].country` | string | No |  |
| `events[].created_at` | string (date-time) | No |  |
| `events[].is_synthetic` | string | No | Allowed values: `true`, `false`. |

**Response example** (`200`)

```json
{
  "ok": true,
  "count": 1,
  "limit": 1,
  "truncated": true,
  "from": "example-from",
  "to": "example-to",
  "next_cursor": "cursor_000123",
  "events": [
    {
      "id": "example-id",
      "device_id": "dev_7a2b9c4d1e6f3a8b5c2d9e4f7a1b6c3d",
      "domain_id": "dom_3f2a8c91d6e5f470",
      "score": 1,
      "risk_level": "low",
      "reason_codes": "example-reason_codes",
      "country": "US",
      "created_at": "2026-09-13T12:00:00.000Z",
      "is_synthetic": "true"
    }
  ]
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/exports/events?from=example-from&to=example-to" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/exports/events?from=example-from&to=example-to', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/exports/events?from=example-from&to=example-to",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Export visits as CSV or JSON {#v1ExportVisits}

`GET /v1/exports/visits`

Bulk-exports visit records for a date range, with the same date validation, per-request row cap and pagination as the other exports. Visits have no synthetic partition, so there is no `source` parameter.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:export`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `format` | query | string | No | Allowed values: `json`, `csv`. Default `json`. |
| `from` | query | string | Yes |  |
| `to` | query | string | Yes |  |
| `domain_id` | query | string | No |  |
| `cursor` | query | string | No |  |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The exported page, as a JSON envelope or a raw CSV body depending on `format`. |  | `application/json`, `text/csv` |
| `400` | `Invalid format`, missing `from`/`to`, or invalid date shape/calendar. | `invalid_request` | `application/json` ErrorEnvelope |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `429` | Rate limit exceeded. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying; the response carries no other rate-limit headers. | `Rate limit exceeded` | `application/json` |
| `500` | Unexpected server fault. Retry with backoff and quote `requestId` to support if it persists. | `Internal server error` | `application/json` ErrorEnvelope |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `200` | `X-Noxtica-Export-Next-Cursor` | string |  |
| `200` | `X-Noxtica-Export-Truncated` | string |  |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `count` | integer | Yes |  |
| `limit` | integer | Yes |  |
| `truncated` | boolean | Yes |  |
| `from` | string | Yes |  |
| `to` | string | Yes |  |
| `next_cursor` | string | No |  |
| `visits` | array of object | Yes |  |
| `visits[]` | object | No |  |
| `visits[].id` | string | No |  |
| `visits[].device_id` | string | No |  |
| `visits[].domain_id` | string | No |  |
| `visits[].country` | string | No |  |
| `visits[].city` | string | No |  |
| `visits[].external_user_id` | string | No |  |
| `visits[].created_at` | string (date-time) | No |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "count": 1,
  "limit": 1,
  "truncated": true,
  "from": "example-from",
  "to": "example-to",
  "next_cursor": "cursor_000123",
  "visits": [
    {
      "id": "example-id",
      "device_id": "dev_7a2b9c4d1e6f3a8b5c2d9e4f7a1b6c3d",
      "domain_id": "dom_3f2a8c91d6e5f470",
      "country": "US",
      "city": "Istanbul",
      "external_user_id": "example-external_user_id",
      "created_at": "2026-09-13T12:00:00.000Z"
    }
  ]
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/exports/visits?from=example-from&to=example-to" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/exports/visits?from=example-from&to=example-to', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/exports/visits?from=example-from&to=example-to",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Stream scored events as JSON Lines {#v1ExportScoredEvents}

`GET /v1/exports/scored-events`

Streams scored events as newline-delimited JSON, newest first: one scored-event object per line, followed by a trailing `_summary` line. The stream is capped at a fixed maximum number of rows per request.

- `tier` is coarser than `gpuAttestationTier` on `GET /v1/device/{deviceId}`: only a hardware-attested device maps to `attested`; everything else is `suspicious`.
- `band` is one of `critical`, `high`, `medium`, `low`, `minimal`; an unrecognised stored value is reported as `minimal`.
- `reason_codes` holds at most 64 entries and is empty unless reason-code output is enabled for your organisation.

This route validates the shape of `from` and `to` only, not calendar validity, unlike the JSON/CSV exports.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:export`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `from` | query | string | Yes | REQUIRED. `YYYY-MM-DD`, `YYYY-MM-DD HH:MM:SS`, or ISO 8601. |
| `to` | query | string | Yes |  |
| `domain_id` | query | string | No |  |
| `cursor` | query | string | No | Opaque keyset cursor from a prior response's `next_cursor` (in the trailing `_summary` line). No cursor param on page 1. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | JSONL body: one scored-event object per line, then a trailing `_summary` line. |  | `application/x-ndjson` |
| `400` | Missing `from`/`to`, or a value not matching the accepted date shape (this route validates SHAPE only, not calendar validity, unlike the sibling JSON/CSV exports). | `invalid_request` | `application/json` ErrorEnvelope |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `429` | Rate limit exceeded. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying; the response carries no other rate-limit headers. | `Rate limit exceeded` | `application/json` |
| `500` | Unexpected server fault. Retry with backoff and quote `requestId` to support if it persists. | `Internal server error` | `application/json` ErrorEnvelope |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `200` | `X-Noxtica-Export-Next-Cursor` | string |  |
| `200` | `X-Noxtica-Export-Truncated` | string |  |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |

**Response fields** (`200`, `application/x-ndjson`)

Type: string.

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/exports/scored-events?from=example-from&to=example-to" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/exports/scored-events?from=example-from&to=example-to', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/exports/scored-events?from=example-from&to=example-to",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Read reason-code firing counts {#v1GetEmergingThreats}

`GET /v1/emerging-threats`

Returns per-reason-code firing counts and rates over a bounded lookback window, aggregated from the published reason-code catalogue. It reuses the `read:export` scope.

The report is disabled unless it is enabled for the deployment. When it is, the route does no warehouse work and answers a neutral `200` with `{"available":false,"reason":"feature_unavailable","signals":[]}`, so you can always probe it safely. A warehouse read fault degrades to an empty `signals` array and `window_sessions: 0`; this route never answers `500` for it.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:export`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `window` | query | integer | No | Lookback hours; any other value silently falls back to 24. Allowed values: `1`, `6`, `24`, `168`, `720`. Default `24`. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The firing-count report, or the feature-disabled neutral empty state. |  | `application/json` |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `429` | Rate limit exceeded. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying; the response carries no other rate-limit headers. | `Rate limit exceeded` | `application/json` |
| `500` | Unexpected server fault. Retry with backoff and quote `requestId` to support if it persists. | `Internal server error` | `application/json` ErrorEnvelope |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `available` | boolean | Yes |  |
| `reason` | string | No | Allowed values: `feature_unavailable`. |
| `window_hours` | integer | Yes |  |
| `window_sessions` | integer | No | Present only when available is true. |
| `signals` | array of object | Yes |  |
| `signals[]` | object | No |  |
| `signals[].reason_code` | string | No |  |
| `signals[].domain` | string | No |  |
| `signals[].label` | string | No |  |
| `signals[].firing_count` | integer | No |  |
| `signals[].firing_rate` | number, nullable | No | `null` when `window_sessions` is below the 20-session display floor. |
| `count` | integer | Yes |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "available": true,
  "reason": "feature_unavailable",
  "window_hours": 1,
  "window_sessions": 1,
  "signals": [
    {
      "reason_code": "example-reason_code",
      "domain": "app.example.com",
      "label": "example-label",
      "firing_count": 1,
      "firing_rate": 1
    }
  ],
  "count": 1
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/emerging-threats" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/emerging-threats', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/emerging-threats",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Fetch the key that opens Sealed Client Results {#v1GetSealedResultsKey}

`GET /v1/sealed-results/key`

Returns your organisation's symmetric key for opening Sealed Client Results envelopes. The key is derived on demand and never stored.

`keyVersion` is optional. Each sealed envelope carries the cleartext `kv` (key version) it was sealed under, and this route serves any past version that has not been revoked, so rotation never orphans an archived envelope. `expiresAt` is present only when a key lifetime is configured.

- `404 sealed_key_version_unavailable` — `keyVersion` is not a positive integer or is newer than the current version. No key material is returned.
- `403` — the key lacks `read:sealed_results`, or the key is revoked for this organisation and version.
- `500 server_misconfigured` — sealed results are not provisioned for this deployment; this is distinct from an authentication failure.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:sealed_results`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `keyVersion` | query | string | No | Key version to fetch — the `kv` a Sealed Client Result envelope carries. Omitted = the current version (the response is unchanged). A past version is served from the server's master ring until it is REVOKED for the tenant (`403`, as for the current version). A malformed or future version answers `404 sealed_key_version_unavailable`. Pattern `^[1-9][0-9]{0,8}$`. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The tenant's sealed-results key material. |  | `application/json` |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | `API key does not have read:sealed_results scope`, OR the sealed-results key is revoked for this tenant/version (`sealed-results key revoked for this tenant/version`). | `invalid_request` | `application/json` |
| `404` | `sealed_key_version_unavailable` — the requested `keyVersion` is not a positive integer, or is newer than the current key version. Explicit (never a 500); no key material is returned. | `sealed-results key version unavailable`, `sealed_key_version_unavailable` | `application/json` |
| `429` | Rate limit exceeded. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying; the response carries no other rate-limit headers. | `Rate limit exceeded` | `application/json` |
| `500` | Unexpected server fault. Retry with backoff and quote `requestId` to support if it persists. | `Internal server error`, `server_misconfigured` | `application/json` ErrorEnvelope |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `keyVersion` | string | Yes |  |
| `key` | string | Yes | base64-encoded raw derived key. |
| `alg` | string | Yes |  |
| `envelope` | string | Yes | Allowed values: `noxtica-sealed-v1`. |
| `expiresAt` | integer | No | Absolute epoch-ms expiry hint; present only when a TTL is configured. |

**Response example** (`200`)

```json
{
  "ok": true,
  "keyVersion": "example-keyVersion",
  "key": "example-key",
  "alg": "example-alg",
  "envelope": "noxtica-sealed-v1",
  "expiresAt": 1768435200
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/sealed-results/key" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/sealed-results/key', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/sealed-results/key",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## List your organisation's ground-truth labels {#v1ListLabels}

`GET /v1/labels`

Lists the labels your organisation has written with `POST /v1/labels`, with optional `device_id` and `label` filters and a keyset cursor over `(created_at, id)` (the same opaque cursor shape as `GET /v1/events`). It reuses the `read:device` scope; there is no separate read-labels scope.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:device`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `device_id` | query | string | No |  |
| `label` | query | string | No | Allowed values: `bot`, `human`, `fraud`, `legit`, `unknown`. |
| `limit` | query | integer | No | Minimum 1, maximum 200. Default `50`. |
| `cursor` | query | string | No |  |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | A page of labels. |  | `application/json` |
| `400` | `Invalid label. Expected one of: bot, human, fraud, legit, unknown`. | `invalid_request` | `application/json` ErrorEnvelope |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `429` | Rate limit exceeded. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying; the response carries no other rate-limit headers. | `Rate limit exceeded` | `application/json` |
| `500` | Unexpected server fault. Retry with backoff and quote `requestId` to support if it persists. | `Internal server error` | `application/json` ErrorEnvelope |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `labels` | array of object | Yes |  |
| `labels[]` | object | No |  |
| `labels[].id` | string (uuid) | No |  |
| `labels[].deviceId` | string | No |  |
| `labels[].eventId` | string, nullable | No |  |
| `labels[].label` | string | No | Allowed values: `bot`, `human`, `fraud`, `legit`, `unknown`. |
| `labels[].source` | string | No |  |
| `labels[].note` | string, nullable | No |  |
| `labels[].createdAt` | string (date-time) | No |  |
| `labels[].updatedAt` | string (date-time) | No |  |
| `hasMore` | boolean | Yes |  |
| `nextCursor` | string, nullable | Yes |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "labels": [
    {
      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "deviceId": "dev_7a2b9c4d1e6f3a8b5c2d9e4f7a1b6c3d",
      "eventId": "evt_3c9b2f7e4a3d4c8e9f1a2b3c4d5e6f70",
      "label": "bot",
      "source": "example-source",
      "note": "created via API",
      "createdAt": "2026-09-13T12:00:00.000Z",
      "updatedAt": "2026-09-13T12:00:00.000Z"
    }
  ],
  "hasMore": true,
  "nextCursor": "cursor_000123"
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/labels" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/labels', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/labels",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Write a ground-truth label for a device {#v1WriteLabel}

`POST /v1/labels`

Records a confirmed fraud, legitimate or other label for a device, optionally scoped to one event, to calibrate scoring. The write is an upsert: repeating the same `(device_id, event_id)` updates `label`, `source`, `note` and `updated_at` on the existing row instead of creating a duplicate.

The body accepts only `device_id`, `label`, `event_id`, `note` and `source`; any other key is rejected with `400 Unknown field`. `label` must be one of `bot`, `human`, `fraud`, `legit`, `unknown`.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `write:labels`.

### Request body

Content type `application/json`, required.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `device_id` | string | Yes | At most 256 characters. |
| `label` | string | Yes | Allowed values: `bot`, `human`, `fraud`, `legit`, `unknown`. |
| `event_id` | string, nullable | No | Optional; blank/whitespace-only normalizes to NULL (collapses onto the device-level idempotency slot). At most 256 characters. |
| `note` | string, nullable | No | At most 1024 characters. |
| `source` | string | No | Truncated to 64 chars; defaults to 'api' when omitted/blank. At most 64 characters. Default `api`. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The upserted (inserted or updated) label row. |  | `application/json` |
| `400` | Unknown body field, missing/oversized `device_id`, invalid `label`, or an oversized `event_id`/`note`. | `invalid_request` | `application/json` ErrorEnvelope |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `413` | The request body exceeded this route's byte cap. | `body_too_large` | `application/json` ErrorEnvelope |
| `415` | `Content-Type must be application/json`. | `invalid_request` | `application/json` ErrorEnvelope |
| `429` | Rate limit exceeded. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying; the response carries no other rate-limit headers. | `Rate limit exceeded` | `application/json` |
| `500` | Unexpected server fault. Retry with backoff and quote `requestId` to support if it persists. | `Internal server error` | `application/json` ErrorEnvelope |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `label` | object | Yes |  |
| `label.id` | string (uuid) | No |  |
| `label.deviceId` | string | No |  |
| `label.eventId` | string, nullable | No |  |
| `label.label` | string | No | Allowed values: `bot`, `human`, `fraud`, `legit`, `unknown`. |
| `label.source` | string | No |  |
| `label.note` | string, nullable | No |  |
| `label.createdAt` | string (date-time) | No |  |
| `label.updatedAt` | string (date-time) | No |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "label": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "deviceId": "dev_7a2b9c4d1e6f3a8b5c2d9e4f7a1b6c3d",
    "eventId": "evt_3c9b2f7e4a3d4c8e9f1a2b3c4d5e6f70",
    "label": "bot",
    "source": "example-source",
    "note": "created via API",
    "createdAt": "2026-09-13T12:00:00.000Z",
    "updatedAt": "2026-09-13T12:00:00.000Z"
  }
}
```

### Example request

**curl**

```bash
curl -X POST "https://collect.noxtica.com/v1/labels" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "device_id": "dev_7a2b9c4d1e6f3a8b5c2d9e4f7a1b6c3d",
  "label": "bot",
  "event_id": "evt_3c9b2f7e4a3d4c8e9f1a2b3c4d5e6f70",
  "note": "created via API",
  "source": "api"
}'
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/labels', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    device_id: 'dev_7a2b9c4d1e6f3a8b5c2d9e4f7a1b6c3d',
    label: 'bot',
    event_id: 'evt_3c9b2f7e4a3d4c8e9f1a2b3c4d5e6f70',
    note: 'created via API',
    source: 'api',
  }),
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/labels",
    data=json.dumps({
        "device_id": "dev_7a2b9c4d1e6f3a8b5c2d9e4f7a1b6c3d",
        "label": "bot",
        "event_id": "evt_3c9b2f7e4a3d4c8e9f1a2b3c4d5e6f70",
        "note": "created via API",
        "source": "api",
    }).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
        "Content-Type": "application/json",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Generate a batch of synthetic devices {#v1GenerateFingerprintBatch}

`POST /v1/generator/fingerprints`

Generates `count` synthetic devices, each with `eventsPerDevice` events, derived deterministically from real template fingerprints, for testing and load. Read them back with `source=synthetic`.

The call is idempotent on `(domain_id, seed)`: a repeated identical request returns the same existing batch with `isExisting: true` and HTTP `200`; a new batch answers `201`. `domain_id` must be an active domain of your organisation, otherwise the route answers `404`.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `write:generate`.

### Request body

Content type `application/json`, required.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `domain_id` | string | Yes |  |
| `seed` | string | Yes | Idempotency key for the whole batch. |
| `count` | integer | No | Coerced via Number(); non-positive/non-finite falls back to 10. Minimum 1, maximum 100. Default `10`. |
| `eventsPerDevice` | integer | No | Minimum 1, maximum 10. Default `3`. |
| `sourceFrom` | string | No |  |
| `sourceTo` | string | No |  |
| `sourceLimit` | integer | No | Maximum 500. Default `200`. |
| `scrubIPs` | boolean | No | Any value other than the literal `false` is treated as true. Default `true`. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The existing batch, when `seed` already matches a committed batch for this `(tenant, domain)`. |  | `application/json` |
| `201` | A newly-committed batch. |  | `application/json` |
| `400` | `domain_id and seed are required`, or `No template fingerprints found in the specified range`. | `invalid_request` | `application/json` ErrorEnvelope |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `404` | `Domain not found or does not belong to tenant`. | `invalid_request` | `application/json` ErrorEnvelope |
| `413` | The request body exceeded this route's byte cap. | `body_too_large` | `application/json` ErrorEnvelope |
| `415` | `Content-Type must be application/json`. | `invalid_request` | `application/json` ErrorEnvelope |
| `500` | Unexpected server fault. Retry with backoff and quote `requestId` to support if it persists. | `Internal server error` | `application/json` ErrorEnvelope |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `isExisting` | boolean | Yes |  |
| `batchId` | string | Yes |  |
| `seed` | string | Yes |  |
| `tenantId` | string | Yes |  |
| `domainId` | string | Yes |  |
| `deviceIds` | array of string | Yes |  |
| `totalDevices` | integer | Yes |  |
| `totalEvents` | integer | Yes |  |
| `createdAt` | string (date-time) | Yes |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "isExisting": true,
  "batchId": "batch_5a1f9c3e7b2d4a6f8091c2d3e4f5a6b7",
  "seed": "seed-01",
  "tenantId": "tenant_42",
  "domainId": "dom_3f2a8c91d6e5f470",
  "deviceIds": [
    "example-deviceIds"
  ],
  "totalDevices": 1,
  "totalEvents": 1,
  "createdAt": "2026-09-13T12:00:00.000Z"
}
```

**Response fields** (`201`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `isExisting` | boolean | Yes |  |
| `batchId` | string | Yes |  |
| `seed` | string | Yes |  |
| `tenantId` | string | Yes |  |
| `domainId` | string | Yes |  |
| `deviceIds` | array of string | Yes |  |
| `totalDevices` | integer | Yes |  |
| `totalEvents` | integer | Yes |  |
| `createdAt` | string (date-time) | Yes |  |

**Response example** (`201`)

```json
{
  "ok": true,
  "isExisting": true,
  "batchId": "batch_5a1f9c3e7b2d4a6f8091c2d3e4f5a6b7",
  "seed": "seed-01",
  "tenantId": "tenant_42",
  "domainId": "dom_3f2a8c91d6e5f470",
  "deviceIds": [
    "example-deviceIds"
  ],
  "totalDevices": 1,
  "totalEvents": 1,
  "createdAt": "2026-09-13T12:00:00.000Z"
}
```

### Example request

**curl**

```bash
curl -X POST "https://collect.noxtica.com/v1/generator/fingerprints" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "domain_id": "dom_3f2a8c91d6e5f470",
  "seed": "seed-01",
  "count": 10,
  "eventsPerDevice": 3,
  "sourceFrom": "example-sourceFrom",
  "sourceTo": "example-sourceTo",
  "sourceLimit": 200,
  "scrubIPs": true
}'
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/generator/fingerprints', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    domain_id: 'dom_3f2a8c91d6e5f470',
    seed: 'seed-01',
    count: 10,
    eventsPerDevice: 3,
    sourceFrom: 'example-sourceFrom',
    sourceTo: 'example-sourceTo',
    sourceLimit: 200,
    scrubIPs: true,
  }),
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/generator/fingerprints",
    data=json.dumps({
        "domain_id": "dom_3f2a8c91d6e5f470",
        "seed": "seed-01",
        "count": 10,
        "eventsPerDevice": 3,
        "sourceFrom": "example-sourceFrom",
        "sourceTo": "example-sourceTo",
        "sourceLimit": 200,
        "scrubIPs": True,
    }).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
        "Content-Type": "application/json",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Generate one seeded synthetic device {#v1GenerateSingleFingerprint}

`POST /v1/generator/fingerprint`

Generates one synthetic device from a filtered selection of real template fingerprints. It is idempotent on the derived seed key: a repeated request returns the existing device unchanged with `isExisting: true` and HTTP `200`; a new device answers `201`.

`json_filters`, when present, constrain the candidate templates by JSONPath-style equality against the stored fingerprint, score and collector fields. Domain ownership, scope and body rules are the same as for `POST /v1/generator/fingerprints`.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `write:generate`.

### Request body

Content type `application/json`, required.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `domain_id` | string | Yes |  |
| `seed` | string | Yes |  |
| `platform` | string | No |  |
| `risk_level` | string | No |  |
| `country` | string | No |  |
| `language` | string | No |  |
| `timezone` | string | No |  |
| `score_min` | number | No |  |
| `score_max` | number | No |  |
| `json_filters` | array of object | No | Normalized/hashed into the deterministic seed key alongside `seed`. |
| `json_filters[]` | object | No |  |
| `json_filters[].source` | string | Yes | Allowed values: `fingerprint`, `score`, `cf`. |
| `json_filters[].path` | string | Yes | A JSONPath-style equality filter, e.g. $.signals.webgl.vendor. |
| `json_filters[].value` | any | No |  |
| `sourceFrom` | string | No |  |
| `sourceTo` | string | No |  |
| `sourceLimit` | integer | No | Maximum 500. Default `200`. |
| `scrubIPs` | boolean | No | Default `true`. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The existing seeded fingerprint (returned unchanged, `Cache-Control: no-store`). |  | `application/json` |
| `201` | A newly-committed seeded fingerprint. |  | `application/json` |
| `400` | `domain_id and seed are required`, or `No template fingerprints found matching the specified filters`. | `invalid_request` | `application/json` ErrorEnvelope |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `404` | `Domain not found or does not belong to tenant`. | `invalid_request` | `application/json` ErrorEnvelope |
| `413` | The request body exceeded this route's byte cap. | `body_too_large` | `application/json` ErrorEnvelope |
| `415` | `Content-Type must be application/json`. | `invalid_request` | `application/json` ErrorEnvelope |
| `500` | Unexpected server fault. Retry with backoff and quote `requestId` to support if it persists. | `Internal server error` | `application/json` ErrorEnvelope |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `200` | `Cache-Control` | string |  |
| `201` | `Cache-Control` | string |  |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `isExisting` | boolean | Yes |  |
| `seed` | string | Yes |  |
| `filters` | object | Yes | The normalized template filters echoed back. |
| `tenantId` | string | Yes |  |
| `domainId` | string | Yes |  |
| `batchId` | string | Yes |  |
| `deviceId` | string | Yes |  |
| `templateEventId` | string | Yes |  |
| `fingerprint` | object | Yes | The stored synthetic fingerprint blob (template-derived; shape not locally schema-validated). |
| `score` | object | Yes | The stored score blob copied verbatim from the selected template. |
| `cf` | object | Yes | The stored Cloudflare-geo blob, IP-scrubbed when scrubIPs is true. |
| `createdAt` | string (date-time) | Yes |  |
| `updatedAt` | string (date-time) | Yes |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "isExisting": true,
  "seed": "seed-01",
  "filters": {},
  "tenantId": "tenant_42",
  "domainId": "dom_3f2a8c91d6e5f470",
  "batchId": "batch_5a1f9c3e7b2d4a6f8091c2d3e4f5a6b7",
  "deviceId": "dev_7a2b9c4d1e6f3a8b5c2d9e4f7a1b6c3d",
  "templateEventId": "example-templateEventId",
  "fingerprint": {},
  "score": {},
  "cf": {},
  "createdAt": "2026-09-13T12:00:00.000Z",
  "updatedAt": "2026-09-13T12:00:00.000Z"
}
```

**Response fields** (`201`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `isExisting` | boolean | Yes |  |
| `seed` | string | Yes |  |
| `filters` | object | Yes | The normalized template filters echoed back. |
| `tenantId` | string | Yes |  |
| `domainId` | string | Yes |  |
| `batchId` | string | Yes |  |
| `deviceId` | string | Yes |  |
| `templateEventId` | string | Yes |  |
| `fingerprint` | object | Yes | The stored synthetic fingerprint blob (template-derived; shape not locally schema-validated). |
| `score` | object | Yes | The stored score blob copied verbatim from the selected template. |
| `cf` | object | Yes | The stored Cloudflare-geo blob, IP-scrubbed when scrubIPs is true. |
| `createdAt` | string (date-time) | Yes |  |
| `updatedAt` | string (date-time) | Yes |  |

**Response example** (`201`)

```json
{
  "ok": true,
  "isExisting": true,
  "seed": "seed-01",
  "filters": {},
  "tenantId": "tenant_42",
  "domainId": "dom_3f2a8c91d6e5f470",
  "batchId": "batch_5a1f9c3e7b2d4a6f8091c2d3e4f5a6b7",
  "deviceId": "dev_7a2b9c4d1e6f3a8b5c2d9e4f7a1b6c3d",
  "templateEventId": "example-templateEventId",
  "fingerprint": {},
  "score": {},
  "cf": {},
  "createdAt": "2026-09-13T12:00:00.000Z",
  "updatedAt": "2026-09-13T12:00:00.000Z"
}
```

### Example request

**curl**

```bash
curl -X POST "https://collect.noxtica.com/v1/generator/fingerprint" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "domain_id": "dom_3f2a8c91d6e5f470",
  "seed": "seed-01",
  "platform": "example-platform",
  "risk_level": "low",
  "country": "US",
  "language": "example-language",
  "timezone": "example-timezone",
  "score_min": 1,
  "score_max": 1,
  "json_filters": [
    {
      "source": "fingerprint",
      "path": "/v1/kyc/verify",
      "value": "example-value"
    }
  ],
  "sourceFrom": "example-sourceFrom",
  "sourceTo": "example-sourceTo",
  "sourceLimit": 200,
  "scrubIPs": true
}'
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/generator/fingerprint', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    domain_id: 'dom_3f2a8c91d6e5f470',
    seed: 'seed-01',
    platform: 'example-platform',
    risk_level: 'low',
    country: 'US',
    language: 'example-language',
    timezone: 'example-timezone',
    score_min: 1,
    score_max: 1,
    json_filters: [
      {
        source: 'fingerprint',
        path: '/v1/kyc/verify',
        value: 'example-value',
      },
    ],
    sourceFrom: 'example-sourceFrom',
    sourceTo: 'example-sourceTo',
    sourceLimit: 200,
    scrubIPs: true,
  }),
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/generator/fingerprint",
    data=json.dumps({
        "domain_id": "dom_3f2a8c91d6e5f470",
        "seed": "seed-01",
        "platform": "example-platform",
        "risk_level": "low",
        "country": "US",
        "language": "example-language",
        "timezone": "example-timezone",
        "score_min": 1,
        "score_max": 1,
        "json_filters": [
            {
                "source": "fingerprint",
                "path": "/v1/kyc/verify",
                "value": "example-value",
            },
        ],
        "sourceFrom": "example-sourceFrom",
        "sourceTo": "example-sourceTo",
        "sourceLimit": 200,
        "scrubIPs": True,
    }).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
        "Content-Type": "application/json",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Read a subject's behavioral baseline {#v1GetBehavioralSubject}

`GET /v1/behavioral/subjects/{subjectRef}`

Reads a subject's current behavioral-baseline enrollment and its latest drift snapshot. `subjectRef` is your raw end-user account id: it is never stored, logged or echoed, only keyed-hashed server-side before any lookup.

Not enabled by default: the whole `/v1/behavioral/*` family answers the `GatedNotFound` 404 unless behavioral baselines are enabled for the deployment, and only then is the `read:behavioral` scope checked.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:behavioral`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `subjectRef` | path | string | Yes | The tenant's raw end-user account id (hashed server-side; never stored as given). At most 256 characters. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The subject's baseline enrollment + latest drift. |  | `application/json` |
| `400` | `Invalid subjectRef` — empty or over 256 chars. | `invalid_request` | `application/json` ErrorEnvelope |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `404` | `Subject not found` (no current snapshot exists), or `V1 endpoint not found` when behavioral baselines are not enabled for the deployment. | `invalid_request` | `application/json` ErrorEnvelope |
| `429` | Rate limit exceeded. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying; the response carries no other rate-limit headers. | `Rate limit exceeded` | `application/json` |
| `500` | Unexpected server fault. Retry with backoff and quote `requestId` to support if it persists. | `Internal server error` | `application/json` ErrorEnvelope |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `subject` | object | Yes |  |
| `subject.enrollmentStatus` | string | No | A persisted value outside the closed set clamps to `learning`. Allowed values: `learning`, `active`. |
| `subject.sampleCount` | integer | No |  |
| `subject.attestationProvenance` | object | No |  |
| `subject.attestationProvenance.gpu_attested` | integer | No |  |
| `subject.attestationProvenance.sealed` | integer | No |  |
| `subject.attestationProvenance.unattested` | integer | No |  |
| `subject.deviation` | number, nullable | No |  |
| `subject.families` | object, nullable | No |  |
| `subject.families.pointer` | number, nullable | No |  |
| `subject.families.keystroke` | number, nullable | No |  |
| `subject.families.cadence` | number, nullable | No |  |
| `subject.lastDriftAt` | string (date-time), nullable | No |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "subject": {
    "enrollmentStatus": "learning",
    "sampleCount": 1,
    "attestationProvenance": {
      "gpu_attested": 1,
      "sealed": 1,
      "unattested": 1
    },
    "deviation": 1,
    "families": {
      "pointer": 1,
      "keystroke": 1,
      "cadence": 1
    },
    "lastDriftAt": "2026-09-13T12:00:00.000Z"
  }
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/behavioral/subjects/example-subjectRef" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/behavioral/subjects/example-subjectRef', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/behavioral/subjects/example-subjectRef",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Erase a subject's behavioral baseline {#v1EraseBehavioralSubject}

`DELETE /v1/behavioral/subjects/{subjectRef}`

Erases a subject's behavioral baseline, for data-subject erasure requests. Live profile state is erased first, then the stored snapshots and drift projections are hard-deleted. If the live-state erase fails the route answers `502` with `{"ok":false,"profileErased":false,...}` instead of a false `200`, because a surviving profile could otherwise re-flush the erased snapshot; the call is safe to retry. Erasing an unknown or already-erased subject is idempotent and answers `200 {"deleted":{"snapshots":0,"driftEvents":0}}`.

This erases account-scoped (`acct:`) subjects only. Device-scoped baselines and the raw behavioral tracks need the separate per-device erasure path in the Backoffice, so a subject with both needs both paths. Availability is the same as for `GET /v1/behavioral/subjects/{subjectRef}`.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `erase:behavioral`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `subjectRef` | path | string | Yes | At most 256 characters. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | Erasure completed (or was already a no-op for an unknown subject). |  | `application/json` |
| `400` | `Invalid subjectRef` — empty or over 256 chars. | `invalid_request` | `application/json` ErrorEnvelope |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `404` | `Subject not found`, or `V1 endpoint not found` when behavioral baselines are not enabled for the deployment. | `invalid_request` | `application/json` ErrorEnvelope |
| `429` | Rate limit exceeded. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying; the response carries no other rate-limit headers. | `Rate limit exceeded` | `application/json` |
| `500` | Unexpected server fault. Retry with backoff and quote `requestId` to support if it persists. | `Internal server error` | `application/json` ErrorEnvelope |
| `502` | `{ ok:false, profileErased:false, deleted, error: 'behavioral profile durable-object erase failed — retry (idempotent)' }` — the erase call is safe to retry. | `invalid_request` | `application/json` |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `profileErased` | boolean | Yes | Allowed values: `true`. |
| `deleted` | object | Yes |  |
| `deleted.snapshots` | integer | No |  |
| `deleted.driftEvents` | integer | No |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "profileErased": true,
  "deleted": {
    "snapshots": 1,
    "driftEvents": 1
  }
}
```

### Example request

**curl**

```bash
curl -X DELETE "https://collect.noxtica.com/v1/behavioral/subjects/example-subjectRef" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/behavioral/subjects/example-subjectRef', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/behavioral/subjects/example-subjectRef",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="DELETE",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Read a subject's drift-event history {#v1GetBehavioralSubjectTimeline}

`GET /v1/behavioral/subjects/{subjectRef}/timeline`

Reads a subject's drift events, newest first. The row cap is clamped to 1–100 and there is no pagination cursor on this route. `subjectRef` handling and availability are the same as for `GET /v1/behavioral/subjects/{subjectRef}`.

**Authentication:** Bearer token (`ApiKeyAuth`), scope `read:behavioral`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `subjectRef` | path | string | Yes | At most 256 characters. |
| `limit` | query | integer | No | Minimum 1, maximum 100. Default `50`. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The subject's drift-event timeline. |  | `application/json` |
| `400` | `Invalid subjectRef` — empty or over 256 chars. | `invalid_request` | `application/json` ErrorEnvelope |
| `401` | The `Authorization: Bearer sk_...` header is missing or malformed, or the key is unknown, revoked, or belongs to a suspended or deleted organisation. The body is deliberately identical for every cause. | `invalid_api_key` | `application/json` InvalidApiKeyError |
| `403` | The API key is valid but does not carry the scope this operation requires; `error` names the missing scope. | `API key does not have read:device scope` | `application/json` ScopeForbiddenError |
| `404` | `Subject not found`, or `V1 endpoint not found` when behavioral baselines are not enabled for the deployment. | `invalid_request` | `application/json` ErrorEnvelope |
| `429` | Rate limit exceeded. Wait `Retry-After` seconds (also `retryAfter` in the body) before retrying; the response carries no other rate-limit headers. | `Rate limit exceeded` | `application/json` |
| `500` | Unexpected server fault. Retry with backoff and quote `requestId` to support if it persists. | `Internal server error` | `application/json` ErrorEnvelope |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the bucket allows another request. |

**Response fields** (`200`, `application/json`)

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `ok` | boolean | Yes | Allowed values: `true`. |
| `events` | array of object | Yes |  |
| `events[]` | object | No |  |
| `events[].deviation` | number, nullable | No |  |
| `events[].families` | object, nullable | No |  |
| `events[].families.pointer` | number, nullable | No |  |
| `events[].families.keystroke` | number, nullable | No |  |
| `events[].families.cadence` | number, nullable | No |  |
| `events[].attestation` | string, nullable | No |  |
| `events[].channel` | string, nullable | No |  |
| `events[].createdAt` | string (date-time) | No |  |
| `count` | integer | Yes |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "events": [
    {
      "deviation": 1,
      "families": {
        "pointer": 1,
        "keystroke": 1,
        "cadence": 1
      },
      "attestation": "example-attestation",
      "channel": "example-channel",
      "createdAt": "2026-09-13T12:00:00.000Z"
    }
  ],
  "count": 1
}
```

### Example request

**curl**

```bash
curl "https://collect.noxtica.com/v1/behavioral/subjects/example-subjectRef/timeline" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

**Node.js** (global `fetch`, Node 18 or later)

```js
const response = await fetch('https://collect.noxtica.com/v1/behavioral/subjects/example-subjectRef/timeline', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

**Python** (standard library)

```python
import os
import urllib.request

request = urllib.request.Request(
    "https://collect.noxtica.com/v1/behavioral/subjects/example-subjectRef/timeline",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

_This page is generated from the Noxtica OpenAPI specification release `v0.4.0`: `v1.yaml` version `1.3.0-2026-10-27` (SHA-256 `3fd15257a022`). Do not edit it by hand._
