---
title: "Protected actions API"
description: "Redeem the request id a browser obtains for a protected action (login, signup, checkout) before your server lets the action proceed."
canonical_url: "https://noxtica.com/docs/api/reference/protected-actions/"
markdown_url: "https://noxtica.com/docs/api/reference/protected-actions/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
---

# Protected actions API

Redeem the request id a browser obtains for a protected action (login, signup, checkout) before your server lets the action proceed.

A protected action works in two steps. Your browser integration declares an action (for example `login`) and receives a `requestId`; your backend then redeems that id once with `POST /v1/protected-actions/verify` and decides whether the action may proceed. Use an `sk_` server API key with the `read:device` scope, the default scope of every key, from your own server. Base URL: `https://collect.noxtica.com`.

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

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

## Redeem a protected-action request id {#postProtectedActionsVerify}

`POST /v1/protected-actions/verify`

Call this from your backend before letting a protected action (login, sign-up, checkout, ...) proceed. Send the `requestId` your browser integration obtained for the action: the resolved value of `noxticaClient.protect(name)`, the hidden `nox_request_id` input of a `data-nox-protect` form, or `form.dataset.noxRequestId` / the `noxtica:protected` event of a script-submitted form. Use an `sk_` key with the `read:device` scope, the default scope of every key.

**Single use inside a 300-second window.** The first valid call consumes the receipt and answers `200` with `consumed: true`. A replay inside the window is `409 already_consumed`. An unknown, expired or other-organisation id is `404 request_not_found`, and after 300 seconds every id is `404`.

**Binding.** `action` is required: an id minted for `login` never satisfies a `checkout` check (`422 action_mismatch`; the receipt is not consumed). `expectedDomain` (a hostname, origin or URL) additionally binds the page that ran the action (`422 domain_mismatch`; not consumed). A request that arrives without a `requestId` (a fail-open timeout, a blocked script) means "no client signal", never a pass.

**Result.** `verdict` is the risk band (`minimal`, `low`, `medium`, `high`, `critical`); `score` (higher is riskier) and `confidence` (0 to 1) are the values the browser received, and this server-to-server call always returns them. The request body is limited to 2048 bytes. A `500` carries no `ok` field and never consumes the receipt.

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

### Request body

Content type `application/json`, required.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `requestId` | string (uuid) | Yes | The protected submission's request id. |
| `action` | string | Yes | The protected action name the tenant expects (`login`, `checkout`, ...). Pattern `^[a-z][a-z0-9_-]{0,31}$`. |
| `expectedDomain` | string | No | Optional hostname/origin/URL the action must have run on. At most 269 characters. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The receipt was valid and has now been consumed. |  | `application/json` |
| `400` | `invalid_body`, `invalid_request_id`, `invalid_action` or `invalid_expected_domain`. | `invalid_action`, `invalid_body`, `invalid_expected_domain`, `invalid_request_id` | `application/json` ProtectedActionVerifyError |
| `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` | `request_not_found` — unknown, expired (older than 300 s) or minted for another tenant. | `request_not_found` | `application/json` ProtectedActionVerifyError |
| `409` | `already_consumed` — the receipt was already redeemed (replay inside the window). | `already_consumed` | `application/json` ProtectedActionVerifyError |
| `413` | `payload_too_large` — over the 2048-byte cap. | `payload_too_large` | `application/json` ProtectedActionVerifyError |
| `422` | `action_mismatch` or `domain_mismatch` — the receipt is NOT consumed. | `action_mismatch`, `domain_mismatch` | `application/json` ProtectedActionVerifyError |
| `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`. |
| `requestId` | string (uuid) | Yes |  |
| `action` | string | Yes |  |
| `fingerprintId` | string | Yes | The device id of the protected submission (the same value the dashboard keys on). |
| `verdict` | string | Yes | Allowed values: `minimal`, `low`, `medium`, `high`, `critical`. |
| `score` | number | Yes |  |
| `confidence` | number | Yes | Minimum 0, maximum 1. |
| `domain` | string | Yes | Hostname of the page that ran the action. |
| `issuedAt` | string (date-time) | Yes |  |
| `expiresAt` | string (date-time) | Yes |  |
| `consumed` | boolean | Yes | This call consumed the receipt. Allowed values: `true`. |
| `consumedAt` | string (date-time) | Yes |  |

**Response example** (`200`)

```json
{
  "ok": true,
  "requestId": "8f1c2a52-6a6e-4b0e-9d52-2a7c1d9e4b10",
  "action": "login",
  "fingerprintId": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
  "verdict": "low",
  "score": 12,
  "confidence": 0.91,
  "domain": "www.example.com",
  "issuedAt": "2026-10-08T10:00:00.000Z",
  "expiresAt": "2026-10-08T10:05:00.000Z",
  "consumed": true,
  "consumedAt": "2026-10-08T10:00:02.000Z"
}
```

### Example request

**curl**

```bash
curl -X POST "https://collect.noxtica.com/v1/protected-actions/verify" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "requestId": "8f1c2a52-6a6e-4b0e-9d52-2a7c1d9e4b10",
  "action": "login",
  "expectedDomain": "www.example.com"
}'
```

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

```js
const response = await fetch('https://collect.noxtica.com/v1/protected-actions/verify', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    requestId: '8f1c2a52-6a6e-4b0e-9d52-2a7c1d9e4b10',
    action: 'login',
    expectedDomain: 'www.example.com',
  }),
});
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/protected-actions/verify",
    data=json.dumps({
        "requestId": "8f1c2a52-6a6e-4b0e-9d52-2a7c1d9e4b10",
        "action": "login",
        "expectedDomain": "www.example.com",
    }).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._
