---
title: "Risk scoring and agent trust API"
description: "Score a request, look up IP and device reputation, verify AI agents, and mint signed crawler vouchers from your server."
canonical_url: "https://noxtica.com/docs/api/reference/score/"
markdown_url: "https://noxtica.com/docs/api/reference/score/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
---

# Risk scoring and agent trust API

Score a request, look up IP and device reputation, verify AI agents, and mint signed crawler vouchers from your server.

Call these operations from your own servers with an `sk_` server API key (`Authorization: Bearer sk_...`). Every call is scoped to your organisation. Base URL: `https://collect.noxtica.com` (production, `sk_prod_` keys).

Most operations need only the `read:device` scope, which every key carries by default. A Score API key created under Integrations & API is a server API key limited to `read:device`: it can call `POST /v1/score` and every other operation that needs only that scope. IP and device reputation (`POST /v1/ip/reputation`, `POST /v1/verify/reputation`) and agent verification are metered against your plan; the reputation module and Agent Passports are not enabled by default.

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

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

## Check email, website and phone reputation {#v1VerifyReputation}

`POST /v1/verify/reputation`

Evaluates the reputation of an email address, a website and/or a phone number supplied by your backend and returns a fused verdict. At least one of `email`, `website` or `phone` is required. The checks are bounded and fail open: a check that cannot complete contributes no signal instead of failing the call.

This operation is not enabled by default. When the reputation module is not enabled for your organisation the route answers the `GatedNotFound` 404, and it does so before the API key is checked. A served call counts toward your organisation's reputation usage. Raw email and website values are not logged.

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

### Request body

Content type `application/json`, required.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | No |  |
| `website` | string | No |  |
| `phone` | string | No |  |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The fused reputation verdict. |  | `application/json` |
| `400` | The request failed validation. `error` is a route-specific string such as `Invalid body` or `invalid_request`; read the operation description for the exact values it can return. | `Invalid body` | `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` | The byte-identical "V1 endpoint not found" response returned when the route's module is not enabled for your organisation, and also for a path that does not exist — indistinguishable by design (anti-enumeration), so do not treat it as proof the path is wrong. | `V1 endpoint 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`. |
| `verdict` | string | Yes |  |
| `signals` | object | Yes | Per-signal sub-scores from `evaluateReputation`; the engine's internal signal-shape union is not locally re-validated by this handler. |
| `confidence` | number | Yes |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "verdict": "example-verdict",
  "signals": {},
  "confidence": 1
}
```

### Example request

**curl**

```bash
curl -X POST "https://collect.noxtica.com/v1/verify/reputation" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

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

```js
const response = await fetch('https://collect.noxtica.com/v1/verify/reputation', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({}),
});
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/verify/reputation",
    data=json.dumps({}).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())
```

## Mint a crawler-access voucher {#v1MintCrawlVoucher}

`POST /v1/crawl/vouchers`

Mints a signed, stateless voucher that a verified crawler (identified by `agentKey`) presents to your Noxtica edge to satisfy a `charge` pricing rule. The edge verifies only the signature and binding. The response's `header` field names the request header that carries the voucher.

The voucher is always scoped to the organisation that owns the API key; a tenant id in the body is never used. The route is fail-closed: it answers `503` when no signing key is available and never emits an unsigned voucher.

Payment capture is not part of this operation. It does not verify that a payment was taken; it only checks that a payment reference (`paymentRef` in the body, or the payment-reference request header) is present, and only requires one when your deployment is configured to. Outstanding vouchers are capped per month, and the cap is claimed only after the voucher has been signed, so a signing failure never consumes it.

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

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Nox-Payment-Ref` | header | string | No | Alternative to the body's `paymentRef`; operator-attested, not independently verified this iteration. |

### Request body

Content type `application/json`, required.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `agentKey` | string | Yes | The crawler identity this voucher pays for ('*' = the catch-all pricing rule). At most 256 characters. |
| `ttlSeconds` | integer | No | Minimum 60, maximum 86400. Default `3600`. |
| `paymentRef` | string | No | Operator-attested payment reference; presence-only checked. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The minted voucher. |  | `application/json` |
| `400` | `agentKey (non-empty string) is required`, or `Invalid JSON body`. | `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 |
| `402` | `Payment context required` — returned only when the deployment requires a payment reference and neither `paymentRef` nor the payment-reference header was supplied. | `invalid_request` | `application/json` ErrorEnvelope |
| `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 |
| `503` | `Crawl voucher minting is not configured` (no signing key is available) or `Voucher quota authority unavailable`. | `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`. |
| `voucher` | string | Yes | `<base64url(payload)>.<base64url(hmac)>`, or the HMAC-signing-trust envelope form when that feature is armed. |
| `expiresAt` | integer | Yes | Epoch seconds. |
| `header` | string | Yes | Allowed values: `X-Nox-Crawl-Voucher`. |

**Response example** (`200`)

```json
{
  "ok": true,
  "voucher": "example-voucher",
  "expiresAt": 1768435200,
  "header": "X-Nox-Crawl-Voucher"
}
```

### Example request

**curl**

```bash
curl -X POST "https://collect.noxtica.com/v1/crawl/vouchers" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "agentKey": "example-agentKey",
  "ttlSeconds": 3600,
  "paymentRef": "example-paymentRef"
}'
```

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

```js
const response = await fetch('https://collect.noxtica.com/v1/crawl/vouchers', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    agentKey: 'example-agentKey',
    ttlSeconds: 3600,
    paymentRef: 'example-paymentRef',
  }),
});
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/crawl/vouchers",
    data=json.dumps({
        "agentKey": "example-agentKey",
        "ttlSeconds": 3600,
        "paymentRef": "example-paymentRef",
    }).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())
```

## Issue a signed Agent Passport (GET) {#getAgentPassport}

`GET /v1/agent/passport`

Runs the same Web Bot Auth verdict as `POST /v1/agent/verify` (RFC 9421 signature verification plus your organisation's agent policy registry) and returns one ES256-signed, offline-verifiable envelope. Verify the signature against the published JWKS (`GET /.well-known/noxtica/agent-passport-jwks.json`, falling back to `/.well-known/noxtica/jwks.json`) by `kid`; no round trip to Noxtica is needed.

The GET form reads only the three Web Bot Auth headers of the incoming request (`signature-input`, `signature`, `signature-agent`) and an optional forwarded attestation from the query parameters `tier`, `anchored`, `riskBand` and `jti`. `sub` is set only for a cryptographically `verified` verdict; every other verdict carries `sub: null`. An unverified `allow` is withheld or downgraded before signing.

**Availability.** Agent Passports are not enabled by default. When they are not enabled for the deployment the route answers `404 {"error":"Not found"}`, a different body from the usual `V1 endpoint not found`; when your organisation has not opted into agent verification it answers `403 {"error":"feature disabled"}`. The route shares its rate-limit bucket with `POST /v1/agent/verify`, and a verified issuance counts toward your agent-verification usage.

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

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `url` | query | string (uri) | No | The signed target URI `@authority` is reconstructed from; degrades to the request's own URL when absent/malformed. |
| `tier` | query | string | No | Optional caller-forwarded attestation tier; a non-member value is silently dropped (fail-open). Allowed values: `hardware`, `software`, `suspicious`, `unavailable`, `failed`. |
| `anchored` | query | string | No | Optional caller-forwarded "server-anchored single-use claim succeeded" flag; only the literal string `true` is honoured. Allowed values: `true`, `false`. |
| `riskBand` | query | string | No | Allowed values: `critical`, `high`, `medium`, `low`, `minimal`. |
| `jti` | query | string | No | OPTIONAL caller-forwarded device-correlation id, echoed back as the passport's untrusted `cid` (never the server-minted replay `jti`). |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The ES256-signed Agent Passport envelope (`SignedEnvelope<AgentPassportPayload>`). A tenant backend verifies this OFFLINE against the published JWKS by `kid` — no round-trip to Noxtica is required. |  | `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` | Your organisation has not opted into agent verification (`feature disabled`). | `feature disabled` | `application/json` |
| `404` | Agent Passports are not enabled for this deployment. The body is `{"error":"Not found"}`, returned before the API key is checked. | `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 fault; the handler otherwise never throws. | `invalid_request` | `application/json` ErrorEnvelope |
| `503` | The signing key is unavailable or invalid (fail-closed: there is no silent fallback to another key). | `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 |
| --- | --- | --- | --- |
| `alg` | string | Yes | Allowed values: `ES256`. |
| `kid` | string | Yes |  |
| `iat` | integer | Yes | Epoch seconds. |
| `exp` | integer | No | Epoch seconds; the signed expiry window (== the payload `ttl`). |
| `kv` | integer | No | OPTIONAL signing key-version; present only when the trust-lifecycle signer is in use. |
| `spv` | integer | No | OPTIONAL signing-policy-version; same presence contract as `kv`. |
| `sig` | string | Yes | base64url ES256 signature over the payload. |
| `payload` | object | Yes |  |
| `payload.typ` | string | Yes | Allowed values: `agent-passport`. |
| `payload.iss` | string | Yes | Allowed values: `noxtica`. |
| `payload.aud` | string | Yes | The authenticated tenant id (audience binding). |
| `payload.pv` | integer | Yes | Passport schema version: 1 = legacy claims, 2 = explicit `callerAsserted` trust marker (when enabled for the deployment). Allowed values: `1`, `2`. |
| `payload.verdict` | string | Yes | Allowed values: `verified`, `unsigned`, `invalid_signature`, `expired`, `replay`. |
| `payload.attestation` | object | Yes |  |
| `payload.attestation.tier` | string | No | Allowed values: `hardware`, `software`, `suspicious`, `unavailable`, `failed`. |
| `payload.attestation.anchored` | boolean | No |  |
| `payload.attestation.provenance` | string | No | Allowed values: `caller_attested`. |
| `payload.callerAsserted` | boolean | No | Present only when `pv:2`. Allowed values: `true`. |
| `payload.riskBand` | string | Yes | Allowed values: `critical`, `high`, `medium`, `low`, `minimal`. |
| `payload.policyAction` | string, nullable | Yes | Allowed values: `allow`, `deny`. |
| `payload.graduatedAction` | string | No | Present only when graduated agent policy is enabled for your organisation. Allowed values: `allow`, `observe`, `challenge`, `deny`. |
| `payload.jti` | string (uuid) | Yes | SERVER-MINTED per-issuance id — the consumer MUST reject a repeated `jti` (replay). |
| `payload.sub` | string, nullable | Yes | The verified subject (keyid, else the signature-agent host); null unless verdict is 'verified'. |
| `payload.tenantId` | string | Yes |  |
| `payload.cid` | string, nullable | Yes | OPTIONAL untrusted caller-forwarded correlation id — never the replay id. |
| `payload.disclosure` | object, nullable | Yes |  |
| `payload.disclosure.required` | boolean | No | Allowed values: `true`. |
| `payload.disclosure.regulation` | string | No | Allowed values: `eu_ai_act_art_50`. |
| `payload.disclosure.obligation` | string | No |  |
| `payload.disclosure.agentVerified` | boolean | No |  |
| `payload.issuedAt` | string (date-time) | Yes |  |
| `payload.ttl` | integer | Yes | Seconds; the signed envelope `exp` window. |

**Response example** (`200`)

```json
{
  "alg": "ES256",
  "kid": "kid-2026-09",
  "iat": 1,
  "exp": 1,
  "kv": 1,
  "spv": 1,
  "sig": "example-sig",
  "payload": {
    "typ": "agent-passport",
    "iss": "noxtica",
    "aud": "example-aud",
    "pv": 1,
    "verdict": "verified",
    "attestation": {
      "tier": "hardware",
      "anchored": true,
      "provenance": "caller_attested"
    },
    "callerAsserted": true,
    "riskBand": "critical",
    "policyAction": "allow",
    "graduatedAction": "allow",
    "jti": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "sub": "example-sub",
    "tenantId": "tenant_42",
    "cid": "example-cid",
    "disclosure": {
      "required": true,
      "regulation": "eu_ai_act_art_50",
      "obligation": "example-obligation",
      "agentVerified": true
    },
    "issuedAt": "2026-09-13T12:00:00.000Z",
    "ttl": 1
  }
}
```

### Example request

**curl**

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

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

```js
const response = await fetch('https://collect.noxtica.com/v1/agent/passport', {
  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/agent/passport",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.status, response.read().decode())
```

## Issue a signed Agent Passport (POST) {#postAgentPassport}

`POST /v1/agent/passport`

Same verdict and signing pipeline as `GET /v1/agent/passport`; the POST form lets your backend forward the signature material in the JSON body instead of request headers, like `POST /v1/agent/verify`. The same availability rules, error responses, rate-limit bucket and usage counting apply.

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

### Request body

Content type `application/json`, optional.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `headers` | object | No | The forwarded Web Bot Auth signature headers (`signature-input`, `signature`, `signature-agent`, lower-cased keys); non-string values are dropped. |
| `headers.*` | string | No |  |
| `url` | string (uri) | No | The signed target URI; a malformed value degrades to the request's own URL. |
| `method` | string | No | The signed HTTP method the signature covers; defaults to `GET` when omitted/blank. |
| `attestation` | object | No |  |
| `attestation.tier` | string | No | Allowed values: `hardware`, `software`, `suspicious`, `unavailable`, `failed`. |
| `attestation.anchored` | boolean | No |  |
| `riskBand` | string | No | Allowed values: `critical`, `high`, `medium`, `low`, `minimal`. |
| `jti` | string | No | OPTIONAL caller-forwarded device-correlation id, echoed back as the passport's untrusted `cid`. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The ES256-signed Agent Passport envelope (`SignedEnvelope<AgentPassportPayload>`). A tenant backend verifies this OFFLINE against the published JWKS by `kid` — no round-trip to Noxtica is required. |  | `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` | Your organisation has not opted into agent verification (`feature disabled`). | `feature disabled` | `application/json` |
| `404` | Agent Passports are not enabled for this deployment. The body is `{"error":"Not found"}`, returned before the API key is checked. | `Not found` | `application/json` |
| `413` | `Payload too large` — the request body exceeded the size limit. | `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 fault. | `invalid_request` | `application/json` ErrorEnvelope |
| `503` | The signing key is unavailable or invalid (fail-closed: there is no silent fallback to another key). | `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 |
| --- | --- | --- | --- |
| `alg` | string | Yes | Allowed values: `ES256`. |
| `kid` | string | Yes |  |
| `iat` | integer | Yes | Epoch seconds. |
| `exp` | integer | No | Epoch seconds; the signed expiry window (== the payload `ttl`). |
| `kv` | integer | No | OPTIONAL signing key-version; present only when the trust-lifecycle signer is in use. |
| `spv` | integer | No | OPTIONAL signing-policy-version; same presence contract as `kv`. |
| `sig` | string | Yes | base64url ES256 signature over the payload. |
| `payload` | object | Yes |  |
| `payload.typ` | string | Yes | Allowed values: `agent-passport`. |
| `payload.iss` | string | Yes | Allowed values: `noxtica`. |
| `payload.aud` | string | Yes | The authenticated tenant id (audience binding). |
| `payload.pv` | integer | Yes | Passport schema version: 1 = legacy claims, 2 = explicit `callerAsserted` trust marker (when enabled for the deployment). Allowed values: `1`, `2`. |
| `payload.verdict` | string | Yes | Allowed values: `verified`, `unsigned`, `invalid_signature`, `expired`, `replay`. |
| `payload.attestation` | object | Yes |  |
| `payload.attestation.tier` | string | No | Allowed values: `hardware`, `software`, `suspicious`, `unavailable`, `failed`. |
| `payload.attestation.anchored` | boolean | No |  |
| `payload.attestation.provenance` | string | No | Allowed values: `caller_attested`. |
| `payload.callerAsserted` | boolean | No | Present only when `pv:2`. Allowed values: `true`. |
| `payload.riskBand` | string | Yes | Allowed values: `critical`, `high`, `medium`, `low`, `minimal`. |
| `payload.policyAction` | string, nullable | Yes | Allowed values: `allow`, `deny`. |
| `payload.graduatedAction` | string | No | Present only when graduated agent policy is enabled for your organisation. Allowed values: `allow`, `observe`, `challenge`, `deny`. |
| `payload.jti` | string (uuid) | Yes | SERVER-MINTED per-issuance id — the consumer MUST reject a repeated `jti` (replay). |
| `payload.sub` | string, nullable | Yes | The verified subject (keyid, else the signature-agent host); null unless verdict is 'verified'. |
| `payload.tenantId` | string | Yes |  |
| `payload.cid` | string, nullable | Yes | OPTIONAL untrusted caller-forwarded correlation id — never the replay id. |
| `payload.disclosure` | object, nullable | Yes |  |
| `payload.disclosure.required` | boolean | No | Allowed values: `true`. |
| `payload.disclosure.regulation` | string | No | Allowed values: `eu_ai_act_art_50`. |
| `payload.disclosure.obligation` | string | No |  |
| `payload.disclosure.agentVerified` | boolean | No |  |
| `payload.issuedAt` | string (date-time) | Yes |  |
| `payload.ttl` | integer | Yes | Seconds; the signed envelope `exp` window. |

**Response example** (`200`)

```json
{
  "alg": "ES256",
  "kid": "kid-2026-09",
  "iat": 1,
  "exp": 1,
  "kv": 1,
  "spv": 1,
  "sig": "example-sig",
  "payload": {
    "typ": "agent-passport",
    "iss": "noxtica",
    "aud": "example-aud",
    "pv": 1,
    "verdict": "verified",
    "attestation": {
      "tier": "hardware",
      "anchored": true,
      "provenance": "caller_attested"
    },
    "callerAsserted": true,
    "riskBand": "critical",
    "policyAction": "allow",
    "graduatedAction": "allow",
    "jti": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "sub": "example-sub",
    "tenantId": "tenant_42",
    "cid": "example-cid",
    "disclosure": {
      "required": true,
      "regulation": "eu_ai_act_art_50",
      "obligation": "example-obligation",
      "agentVerified": true
    },
    "issuedAt": "2026-09-13T12:00:00.000Z",
    "ttl": 1
  }
}
```

### Example request

**curl**

```bash
curl -X POST "https://collect.noxtica.com/v1/agent/passport" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "headers": {
    "key": "example-headers"
  },
  "url": "https://verify.noxtica.com/s/kycs.tenant_42.9f3a8b7c6d5e4f3a2b1c0d9e8f7a6b5c",
  "method": "example-method",
  "attestation": {
    "tier": "hardware",
    "anchored": true
  },
  "riskBand": "critical",
  "jti": "example-jti"
}'
```

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

```js
const response = await fetch('https://collect.noxtica.com/v1/agent/passport', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    headers: {
      key: 'example-headers',
    },
    url: 'https://verify.noxtica.com/s/kycs.tenant_42.9f3a8b7c6d5e4f3a2b1c0d9e8f7a6b5c',
    method: 'example-method',
    attestation: {
      tier: 'hardware',
      anchored: true,
    },
    riskBand: 'critical',
    jti: 'example-jti',
  }),
});
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/agent/passport",
    data=json.dumps({
        "headers": {
            "key": "example-headers",
        },
        "url": "https://verify.noxtica.com/s/kycs.tenant_42.9f3a8b7c6d5e4f3a2b1c0d9e8f7a6b5c",
        "method": "example-method",
        "attestation": {
            "tier": "hardware",
            "anchored": True,
        },
        "riskBand": "critical",
        "jti": "example-jti",
    }).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())
```

## Verify a Web Bot Auth agent signature {#postAgentVerify}

`POST /v1/agent/verify`

Your backend forwards an incoming request's signature headers and target URL. Noxtica verifies the Web Bot Auth (RFC 9421) signature and joins the verdict to your organisation's agent policy registry for an allow or deny decision.

Verification fails open end to end: any fault yields verdict `unsigned` and `matchedAction: null`. The endpoint is advisory and never blocks an agent itself. A structurally matched but unverified identity can never receive a binary `allow` (it is withheld to `null`), and a graduated `allow` is downgraded to `challenge`.

Some response fields are additive and appear only when the matching capability is enabled for your organisation: `kya` and `policyAction` (the graduated ladder) and `disclosure` (EU AI Act Article 50 disclosure, only for a detected or verified agent).

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

### Request body

Content type `application/json`, required.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `headers` | object | Yes | The forwarded Web Bot Auth signature headers (`signature-input`, `signature`, `signature-agent`). |
| `headers.*` | string | No |  |
| `url` | string (uri) | No | The signed target URI the signature covers. |
| `attestation` | object | No |  |
| `attestation.tier` | string | No | Allowed values: `hardware`, `software`, `suspicious`, `unavailable`, `failed`. |
| `attestation.anchored` | boolean | No |  |
| `riskBand` | string | No | Allowed values: `critical`, `high`, `medium`, `low`, `minimal`. |
| `jti` | string | No | Device-correlated token id; when graduated agent policy is enabled for your organisation and it has registry rows, it completes an `agent_attestation_chain` link. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | A resolved Web Bot Auth verdict plus the KYA registry join. |  | `application/json` |
| `400` | The request failed validation. `error` is a route-specific string such as `Invalid body` or `invalid_request`; read the operation description for the exact values it can return. | `Invalid body` | `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`. |
| `verdict` | string | Yes | Allowed values: `verified`, `unsigned`, `invalid_signature`, `expired`, `replay`. |
| `keyid` | string, nullable | Yes |  |
| `signatureAgent` | string, nullable | Yes |  |
| `matchedAction` | string, nullable | Yes | Allowed values: `allow`, `deny`. |
| `attestation` | object | No | Present ONLY when the caller forwarded a valid `tier`. |
| `attestation.tier` | string | No | Allowed values: `hardware`, `software`, `suspicious`, `unavailable`, `failed`. |
| `attestation.anchored` | boolean | No |  |
| `attestation.provenance` | string | No | Always `caller_attested` — this handler never independently corroborates the forwarded tier. Allowed values: `caller_attested`. |
| `riskBand` | string | No | Allowed values: `critical`, `high`, `medium`, `low`, `minimal`. |
| `kya` | object | No | Present only when graduated agent policy is enabled for your organisation. |
| `kya.verified` | boolean | No |  |
| `kya.agentId` | string, nullable | No |  |
| `kya.source` | string | No | Allowed values: `web-bot-auth`, `skyfire`. |
| `kya.attestationTier` | string | No | Allowed values: `hardware`, `software`, `suspicious`, `unavailable`, `failed`. |
| `kya.attestationTierProvenance` | string | No | Allowed values: `caller_attested`. |
| `kya.cfVerified` | boolean | No | Present ONLY on a Cloudflare Verified AI Agents match. |
| `kya.cfVerifiedName` | string | No |  |
| `kya.trustCaveat` | string | No | Present ONLY when the identity came from the Skyfire interop adapter. Allowed values: `structural_only_signature_unverified`. |
| `disclosure` | object | No | Present only when EU AI Act disclosure is enabled for your organisation and a detected or verified agent is present. |
| `disclosure.required` | boolean | No | Allowed values: `true`. |
| `disclosure.regulation` | string | No | Allowed values: `eu_ai_act_art_50`. |
| `disclosure.obligation` | string | No |  |
| `disclosure.agentVerified` | boolean | No |  |
| `policyAction` | string | No | Present only when graduated agent policy is enabled for your organisation and an attributable identity exists — the advisory graduated enforcement tier (Noxtica never acts on it). Allowed values: `allow`, `observe`, `challenge`, `deny`. |

**Response example** (`200`)

```json
{
  "ok": true,
  "verdict": "verified",
  "keyid": "example-keyid",
  "signatureAgent": "example-signatureAgent",
  "matchedAction": "allow",
  "attestation": {
    "tier": "hardware",
    "anchored": true,
    "provenance": "caller_attested"
  },
  "riskBand": "critical",
  "kya": {
    "verified": true,
    "agentId": "example-agentId",
    "source": "web-bot-auth",
    "attestationTier": "hardware",
    "attestationTierProvenance": "caller_attested",
    "cfVerified": true,
    "cfVerifiedName": "example-cfVerifiedName",
    "trustCaveat": "structural_only_signature_unverified"
  },
  "disclosure": {
    "required": true,
    "regulation": "eu_ai_act_art_50",
    "obligation": "example-obligation",
    "agentVerified": true
  },
  "policyAction": "allow"
}
```

### Example request

**curl**

```bash
curl -X POST "https://collect.noxtica.com/v1/agent/verify" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "headers": {
    "key": "example-headers"
  },
  "url": "https://verify.noxtica.com/s/kycs.tenant_42.9f3a8b7c6d5e4f3a2b1c0d9e8f7a6b5c",
  "attestation": {
    "tier": "hardware",
    "anchored": true
  },
  "riskBand": "critical",
  "jti": "example-jti"
}'
```

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

```js
const response = await fetch('https://collect.noxtica.com/v1/agent/verify', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    headers: {
      key: 'example-headers',
    },
    url: 'https://verify.noxtica.com/s/kycs.tenant_42.9f3a8b7c6d5e4f3a2b1c0d9e8f7a6b5c',
    attestation: {
      tier: 'hardware',
      anchored: true,
    },
    riskBand: 'critical',
    jti: 'example-jti',
  }),
});
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/agent/verify",
    data=json.dumps({
        "headers": {
            "key": "example-headers",
        },
        "url": "https://verify.noxtica.com/s/kycs.tenant_42.9f3a8b7c6d5e4f3a2b1c0d9e8f7a6b5c",
        "attestation": {
            "tier": "hardware",
            "anchored": True,
        },
        "riskBand": "critical",
        "jti": "example-jti",
    }).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())
```

## Look up IP reputation {#postIpReputation}

`POST /v1/ip/reputation`

Returns the reputation verdict `{category, source, confidence}` for one IP address supplied by your backend, from the same feed Noxtica uses to score its own submissions. When the address is not listed the response is `listed: false`; an unlisted address, an empty feed or a lookup fault never produces an error.

The queried IP is echoed back in the response, which is your own input. It is not stored or logged unless your organisation has opted into raw-IP storage; only the derived verdict is retained. A served call counts toward your organisation's IP-reputation usage.

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

### Request body

Content type `application/json`, required.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `ip` | string | Yes | An IPv4 dotted-quad or IPv6 address. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The derived reputation verdict, or `listed:false` when the engine has no signal. |  | `application/json` |
| `400` | The request failed validation. `error` is a route-specific string such as `Invalid body` or `invalid_request`; read the operation description for the exact values it can return. | `Invalid body` | `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`. |
| `ip` | string | Yes | Echoed back verbatim (the caller's own query input). |
| `listed` | boolean | Yes |  |
| `verdict` | object | No | Present ONLY when `listed` is true. |
| `verdict.category` | string | No |  |
| `verdict.source` | string | No |  |
| `verdict.confidence` | number | No |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "ip": "example-ip",
  "listed": true,
  "verdict": {
    "category": "example-category",
    "source": "example-source",
    "confidence": 1
  }
}
```

### Example request

**curl**

```bash
curl -X POST "https://collect.noxtica.com/v1/ip/reputation" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "ip": "example-ip"
}'
```

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

```js
const response = await fetch('https://collect.noxtica.com/v1/ip/reputation', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    ip: 'example-ip',
  }),
});
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/ip/reputation",
    data=json.dumps({
        "ip": "example-ip",
    }).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())
```

## Score a signals payload {#postScore}

`POST /v1/score`

Scores an already-collected `signals` payload from your backend and returns the headline score, confidence, tier and flags that the Noxtica collector computes for its own submissions. Call it from a server with an `sk_` key that carries the `read:device` scope (the default scope of every key); never from a browser.

**Request.** Send either the complete enriched payload `{"signals": {...}, "serverSideSignals": {...}, ...}` or, for older callers, the inner `signals` object on its own, which is wrapped once. The body may be up to 256 KiB. The raw payload is not logged.

**Response.** `score` is the engine's score, `tier` its risk band and `flags` the flags that fired. `confidence` is an integer from 0 to 100 (50 is neutral). `attestation` is always `null` on this route: the payload is supplied by the caller and cannot be trusted as hardware evidence, so no hardware-verification claim is ever made.

**Allowance.** Calls within your plan's monthly Score API allowance are included. Beyond it the score is still served and the response carries a `nudge` object with `code: free_tier_exceeded`; the score is never withheld.

**Errors.** `400` `Invalid body` when the body is not a JSON object or is larger than 256 KiB, and `Invalid signals` when `signals` is present but is not an object; `401`, `403`, `429` and `500` as on every key-authenticated route. No `413` is returned: an oversized body is a `400`.

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

### Request body

Content type `application/json`, required.

Type: object.

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The re-scored verdict, mapped onto the public 0-100 confidence scale. |  | `application/json` |
| `400` | The request failed validation. `error` is a route-specific string such as `Invalid body` or `invalid_request`; read the operation description for the exact values it can return. | `Invalid body` | `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`. |
| `score` | number | Yes |  |
| `confidence` | integer | Yes | Normalised to 0-100 (the engine's native value is 0-1), matching `/v1/attest/verify`'s scale. Minimum 0, maximum 100. |
| `tier` | string | Yes | The engine's `riskLevel` band. |
| `flags` | array of string | Yes |  |
| `attestation` | any, nullable | Yes | ALWAYS `null` on this surface — see the honesty-gate note above. |
| `nudge` | object | No | Present ONLY once the free monthly allotment is exceeded — informational only, never withholds the score. |
| `nudge.code` | string | No | Allowed values: `free_tier_exceeded`. |
| `nudge.message` | string | No |  |
| `nudge.free_serving_allotment` | integer | No |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "score": 1,
  "confidence": 0,
  "tier": "example-tier",
  "flags": [
    "example-flags"
  ],
  "attestation": null,
  "nudge": {
    "code": "free_tier_exceeded",
    "message": "example-message",
    "free_serving_allotment": 1
  }
}
```

### Example request

**curl**

```bash
curl -X POST "https://collect.noxtica.com/v1/score" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

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

```js
const response = await fetch('https://collect.noxtica.com/v1/score', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({}),
});
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/score",
    data=json.dumps({}).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())
```

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