---
title: "Pulse tenant API"
description: "Read player status and decisions, create link codes, and manage links from your own back end with a tenant API key."
canonical_url: "https://noxtica.com/docs/api/reference/pulse/"
markdown_url: "https://noxtica.com/docs/api/reference/pulse/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: "0.9.0-2026-10-27"
site: "Noxtica"
language: "en"
source: markdown
---

# Pulse tenant API

Read player status and decisions, create link codes, and manage links from your own back end with a tenant API key.

| Environment | Base URL |
| --- | --- |
| Production (not yet deployed) | `https://pulse.noxtica.com` |

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

## Status of one player (pseudonymous subject) of the calling tenant {#pulseTenantSubjectStatus}

`GET /pulse/v1/tenant/subjects/{subject}`

Server-to-server, scope `read:pulse`. `subject` is a `psub_…` enrolment pseudonym, the pairwise link subject `plnk_…` or the tenant's own `externalUserId` (both resolve ONLY through an ACTIVE link: after an unlink they answer `404 subject_not_found`; an `externalUserId` linked in several applications is `409 ambiguous_subject` unless `?appId=` names one). A subject resolved through a link shows only the scopes the player consented to (`status`: `enrolled`/`revokedAt`/`reasonCode`; `attestation_level`: `deviceAttestationLevel`; `last_seen`: `lastSeen`; `open_signals`: `openSignals`) and echoes them in `scopes`. The tenant is always the key's tenant: unknown subjects and other tenants' subjects are the same `404 subject_not_found`. The answer carries no account PII and no enrolment, device or key identifier. A revoked subject answers `enrolled: false` with `revokedAt` and `reasonCode`. `openSignals` are the subject's open detections (`open`, `under_review`), newest first, at most 50, with tenant-safe fields only; `[]` when there are none. `Cache-Control: no-store`. Times are epoch milliseconds.

**Authentication:** Bearer token (`tenantKey`), scope `read:pulse`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `subject` | path | string | Yes | At least 1 character, at most 256 characters. |
| `appId` | query | string | No | Narrows an `externalUserId` that is linked in several applications of the tenant (else `409 ambiguous_subject`). Pattern `^[A-Za-z0-9_.:-]{1,64}$`. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The subject's status. |  | `application/json` TenantSubjectStatus |
| `400` | `invalid_subject` (undecodable, control characters or longer than 256). | `invalid_subject` | `application/json` ApiError |
| `401` | `missing_api_key` or `invalid_api_key` (malformed, unknown, revoked, or a device token). `WWW-Authenticate: Bearer realm="pulse-tenant"`. | `invalid_api_key`, `missing_api_key` | `application/json` ApiError |
| `403` | `insufficient_scope` (the key lacks `read:pulse`) or `tenant_not_armed` (the tenant is not active and Pulse-enabled). | `insufficient_scope`, `tenant_not_armed` | `application/json` ApiError |
| `404` | `subject_not_found`. | `subject_not_found` | `application/json` ApiError |
| `409` | `ambiguous_subject` — the `externalUserId` is linked in several applications: pass `?appId=`. | `ambiguous_subject` | `application/json` ApiError |
| `429` | Too many attempts. |  | `application/json` ApiError |
| `503` | `tenant_auth_unavailable` or `rate_limiter_unavailable` (fail closed). | `rate_limiter_unavailable`, `tenant_auth_unavailable` | `application/json` ApiError |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `200` | `Cache-Control` | const |  |
| `429` | `Retry-After` | integer | Seconds until the window resets. |

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

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `schemaVersion` | const | Yes | Always `pulse.tenant.subject-status/v1`. |
| `scopes` | array of string | No | Present only for a subject resolved through a link: the scopes the player granted. |
| `subject` | string | Yes | The subject exactly as asked. |
| `enrolled` | boolean | No | At least one active enrolment. |
| `deviceAttestationLevel` | one of string or any, nullable | No | Highest attestation level among the active enrolments; `null` when not enrolled. |
| `attestation` | one of TenantAttestation or any, nullable | No | DET-1, additive (same `attestation_level` link scope as `deviceAttestationLevel`). The attestation facts of the best active enrolment; `null` when it never attested; absent when the subject is not enrolled. |
| `attestation{TenantAttestation}` | object (TenantAttestation) | No | DET-1, additive. The attestation facts of the active enrolment with the highest EFFECTIVE level (ties: most recent statement). No posture values and no PCR values. The level is the effective one: an L3 past its expiry reads as its base level with `attestation_stale`. |
| `attestation{TenantAttestation}.level` | string | Yes | Allowed values: `L0`, `L1`, `L2`, `L3`. |
| `attestation{TenantAttestation}.reasonCodes` | array of string | Yes |  |
| `attestation{TenantAttestation}.attestedAt` | one of integer (int64) or any, nullable | Yes |  |
| `attestation{TenantAttestation}.expiresAt` | one of integer (int64) or any, nullable | Yes |  |
| `attestation{TenantAttestation}.policyRevision` | one of integer or any, nullable | Yes |  |
| `lastSeen` | one of integer (int64) or any, nullable | No | Latest control-plane activity (epoch ms) of any enrolment of the subject, `null` when never. |
| `openSignals` | array of TenantOpenSignal | No | At most 50 items. |
| `openSignals[]` | object (TenantOpenSignal) | No |  |
| `openSignals[].reasonCode` | string | Yes | Pattern `^[A-Z]{3}-[0-9]{3}$`. |
| `openSignals[].severity` | string | Yes | Allowed values: `low`, `medium`, `high`, `critical`. |
| `openSignals[].tier` | string | Yes | Allowed values: `t0`, `t1`, `t2`. |
| `openSignals[].status` | string | Yes | Allowed values: `open`, `under_review`. |
| `openSignals[].detectedAt` | integer (int64) | Yes |  |
| `responseHints` | array of TenantResponseHint | No | Additive. The subject's ACTIVE response hints (an executed `suspend_session_hint` that passed the server's Art. 22 gate, not reversed, not expired), newest first; `[]` when there are none. Never a dry run, a suppressed action or a notice. A hint is a signal for the tenant's own decision. For a link subject present only when the player granted `open_signals`. |
| `responseHints[]` | object (TenantResponseHint) | No |  |
| `responseHints[].decisionId` | string | Yes | The `pact_…` id of the decision (`GET /pulse/v1/tenant/decisions/{id}`). |
| `responseHints[].action` | const | Yes | Always `suspend_session_hint`. |
| `responseHints[].reasonCode` | string | Yes |  |
| `responseHints[].severity` | string | Yes | Allowed values: `low`, `medium`, `high`, `critical`. |
| `responseHints[].issuedAt` | integer (int64) | Yes | When the hint began: `min(observed_at, recorded_at)` of the decision (equals `hint.issuedAt` of `GET /pulse/v1/tenant/decisions/{id}`). |
| `responseHints[].expiresAt` | integer (int64) | Yes |  |
| `revokedAt` | integer (int64) | No | Present only when `enrolled` is false. |
| `reasonCode` | string | No | Present only when `enrolled` is false; the closed revocation reason (e.g. `player_removed`, `tenant_revoked`, `refresh_reuse`). |

### Example request

**curl**

```bash
curl "https://pulse.noxtica.com/pulse/v1/tenant/subjects/psub_01M4GW6CR553W8AAWXEG17RDJW" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

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

```js
const response = await fetch('https://pulse.noxtica.com/pulse/v1/tenant/subjects/psub_01M4GW6CR553W8AAWXEG17RDJW', {
  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://pulse.noxtica.com/pulse/v1/tenant/subjects/psub_01M4GW6CR553W8AAWXEG17RDJW",
    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 the tenant's response decisions (audit), newest first {#pulseTenantListDecisions}

`GET /pulse/v1/tenant/decisions`

Server-to-server, scope `read:pulse`. Every response action the device executed, suppressed or dry-ran under the tenant's signed profile, with its reason code, the outcome of record, the linked detection's review status and appeal state, and the hint state. Keyset-paged (`cursor` from `nextCursor`). Pseudonyms (`psub`, `deviceRef`) and closed codes only: no account id, e-mail, enrolment id, evidence, profile hash or staff identity. `subject` resolves like the subject status; for a linked subject the player must have granted `open_signals` (`403 scope_not_granted`). `Cache-Control: no-store`.

**Authentication:** Bearer token (`tenantKey`), scope `read:pulse`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `app_id` | query | string | No | Pattern `^[a-z0-9][a-z0-9-]{0,62}$`. |
| `device_ref` | query | string | No | The `pdrf_` pseudonym of the device (server-side filter: the page and `nextCursor` only walk this device's decisions). Pattern `^pdrf_[0-9A-HJKMNP-TV-Z]{26}$`. |
| `severity` | query | string | No | Allowed values: `low`, `medium`, `high`, `critical`. |
| `subject` | query | string | No | At least 1 character, at most 256 characters. |
| `outcome` | query | string | No | Allowed values: `executed`, `dry_run`, `suppressed_rate_limit`, `suppressed_cooldown`, `suppressed_kill_switch`, `suppressed_requires_human`. |
| `action` | query | string | No | The closed local-action allow-list of the response executor. Nothing else is representable. Allowed values: `notify_user`, `suspend_session_hint`. |
| `reason_code` | query | string | No | Pattern `^[A-Z]{3}-[0-9]{3}$`. |
| `rule_id` | query | string | No | At most 64 characters. |
| `hint_state` | query | string | No | Allowed values: `active`, `expired`, `reversed`, `none`. |
| `detection_id` | query | string | No | Pattern `^pdet_[0-9A-HJKMNP-TV-Z]{26}$`. |
| `from` | query | integer (int64) | No | Epoch ms (inclusive) of the device decision. Minimum 1. |
| `to` | query | integer (int64) | No | Epoch ms (exclusive). Minimum 1. |
| `limit` | query | integer | No | Minimum 1, maximum 200. Default `50`. |
| `cursor` | query | string | No | At most 96 characters. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | One page of decisions. |  | `application/json` TenantDecisionList |
| `400` | `invalid_request` (a malformed filter) or `invalid_cursor`. | `invalid_cursor`, `invalid_request` | `application/json` ApiError |
| `401` | `missing_api_key` or `invalid_api_key` (a device token is never accepted). | `invalid_api_key`, `missing_api_key` | `application/json` ApiError |
| `403` | `insufficient_scope`, `tenant_not_armed` or `scope_not_granted`. | `insufficient_scope`, `scope_not_granted`, `tenant_not_armed` | `application/json` ApiError |
| `404` | `subject_not_found`. | `subject_not_found` | `application/json` ApiError |
| `429` | Too many attempts. |  | `application/json` ApiError |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `200` | `Cache-Control` | const |  |
| `429` | `Retry-After` | integer | Seconds until the window resets. |

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

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `schemaVersion` | const | Yes | Always `pulse.tenant.decisions/v1`. |
| `decisions` | array of TenantDecision | Yes |  |
| `decisions[]` | object (TenantDecision) | No |  |
| `decisions[].schemaVersion` | const | Yes | Always `pulse.tenant.decision/v1`. |
| `decisions[].id` | string | Yes | Pattern `^pact_[0-7][0-9A-HJKMNP-TV-Z]{25}$`. |
| `decisions[].appId` | string | Yes |  |
| `decisions[].subject` | object | Yes |  |
| `decisions[].subject.psub` | string | Yes |  |
| `decisions[].subject.deviceRef` | string | Yes |  |
| `decisions[].action` | string | Yes | The closed local-action allow-list of the response executor. Nothing else is representable. Allowed values: `notify_user`, `suspend_session_hint`. |
| `decisions[].outcome` | string | Yes | The outcome of record after the server's gates (a report the server downgraded reads `suppressed_kill_switch`, `suppressed_requires_human` or `dry_run`). Allowed values: `executed`, `dry_run`, `suppressed_rate_limit`, `suppressed_cooldown`, `suppressed_kill_switch`, `suppressed_requires_human`. |
| `decisions[].reason` | string | Yes |  |
| `decisions[].reasonCode` | string | Yes |  |
| `decisions[].severity` | string | Yes | Allowed values: `low`, `medium`, `high`, `critical`. |
| `decisions[].evidenceTier` | string | Yes | Allowed values: `t0`, `t1`. |
| `decisions[].ruleId` | string | Yes |  |
| `decisions[].ladderId` | string | Yes |  |
| `decisions[].bundleRevision` | integer | Yes |  |
| `decisions[].observedAt` | integer (int64) | Yes |  |
| `decisions[].recordedAt` | integer (int64) | Yes |  |
| `decisions[].detection` | one of object or any, nullable | Yes | The detection the decision answers; `status` and `appealState` are `null` while its event has not been ingested. |
| `decisions[].detection{variant 1}` | object | No |  |
| `decisions[].detection{variant 1}.id` | string | Yes |  |
| `decisions[].detection{variant 1}.status` | string, nullable | Yes | Allowed values: `open`, `under_review`, `confirmed`, `dismissed`, `overturned`, `suppressed`, `null`. |
| `decisions[].detection{variant 1}.appealState` | string, nullable | Yes | Allowed values: `none`, `open`, `upheld`, `overturned`, `withdrawn`, `null`. |
| `decisions[].hint` | one of object or any, nullable | Yes | `null` when the decision produced no hint. |
| `decisions[].hint{variant 1}` | object | No |  |
| `decisions[].hint{variant 1}.state` | string | Yes | Allowed values: `active`, `expired`, `reversed`. |
| `decisions[].hint{variant 1}.issuedAt` | integer (int64) | Yes | When the hint began: `min(observed_at, recorded_at)` — never later than the server saw the report. Equals `responseHints[].issuedAt` of the subject status. |
| `decisions[].hint{variant 1}.expiresAt` | integer (int64) | Yes |  |
| `decisions[].hint{variant 1}.reversedAt` | integer (int64), nullable | Yes |  |
| `decisions[].hint{variant 1}.reversalSource` | string, nullable | Yes | Allowed values: `staff`, `tenant_api`, `appeal_overturned`, `detection_dismissed`, `detection_overturned`, `detection_suppressed`, `null`. |
| `decisions[].hint{variant 1}.reversalReasonCode` | string, nullable | Yes | Allowed values: `handled`, `false_positive`, `account_verified`, `policy_exception`, `other`, `null`. |
| `nextCursor` | string, nullable | Yes | Opaque; `null` on the last page. |

### Example request

**curl**

```bash
curl "https://pulse.noxtica.com/pulse/v1/tenant/decisions" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

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

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

## One response decision {#pulseTenantGetDecision}

`GET /pulse/v1/tenant/decisions/{id}`

Scope `read:pulse`. `404 decision_not_found` for an unknown or another tenant's id (indistinguishable).

**Authentication:** Bearer token (`tenantKey`), scope `read:pulse`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes | Pattern `^pact_[0-7][0-9A-HJKMNP-TV-Z]{25}$`. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The decision. |  | `application/json` TenantDecision |
| `401` | `missing_api_key` or `invalid_api_key`. | `invalid_api_key`, `missing_api_key` | `application/json` ApiError |
| `403` | `insufficient_scope` or `tenant_not_armed`. | `insufficient_scope`, `tenant_not_armed` | `application/json` ApiError |
| `404` | `decision_not_found`. | `decision_not_found` | `application/json` ApiError |
| `429` | Too many attempts. |  | `application/json` ApiError |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the window resets. |

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

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `schemaVersion` | const | Yes | Always `pulse.tenant.decision/v1`. |
| `id` | string | Yes | Pattern `^pact_[0-7][0-9A-HJKMNP-TV-Z]{25}$`. |
| `appId` | string | Yes |  |
| `subject` | object | Yes |  |
| `subject.psub` | string | Yes |  |
| `subject.deviceRef` | string | Yes |  |
| `action` | string | Yes | The closed local-action allow-list of the response executor. Nothing else is representable. Allowed values: `notify_user`, `suspend_session_hint`. |
| `outcome` | string | Yes | The outcome of record after the server's gates (a report the server downgraded reads `suppressed_kill_switch`, `suppressed_requires_human` or `dry_run`). Allowed values: `executed`, `dry_run`, `suppressed_rate_limit`, `suppressed_cooldown`, `suppressed_kill_switch`, `suppressed_requires_human`. |
| `reason` | string | Yes |  |
| `reasonCode` | string | Yes |  |
| `severity` | string | Yes | Allowed values: `low`, `medium`, `high`, `critical`. |
| `evidenceTier` | string | Yes | Allowed values: `t0`, `t1`. |
| `ruleId` | string | Yes |  |
| `ladderId` | string | Yes |  |
| `bundleRevision` | integer | Yes |  |
| `observedAt` | integer (int64) | Yes |  |
| `recordedAt` | integer (int64) | Yes |  |
| `detection` | one of object or any, nullable | Yes | The detection the decision answers; `status` and `appealState` are `null` while its event has not been ingested. |
| `detection{variant 1}` | object | No |  |
| `detection{variant 1}.id` | string | Yes |  |
| `detection{variant 1}.status` | string, nullable | Yes | Allowed values: `open`, `under_review`, `confirmed`, `dismissed`, `overturned`, `suppressed`, `null`. |
| `detection{variant 1}.appealState` | string, nullable | Yes | Allowed values: `none`, `open`, `upheld`, `overturned`, `withdrawn`, `null`. |
| `hint` | one of object or any, nullable | Yes | `null` when the decision produced no hint. |
| `hint{variant 1}` | object | No |  |
| `hint{variant 1}.state` | string | Yes | Allowed values: `active`, `expired`, `reversed`. |
| `hint{variant 1}.issuedAt` | integer (int64) | Yes | When the hint began: `min(observed_at, recorded_at)` — never later than the server saw the report. Equals `responseHints[].issuedAt` of the subject status. |
| `hint{variant 1}.expiresAt` | integer (int64) | Yes |  |
| `hint{variant 1}.reversedAt` | integer (int64), nullable | Yes |  |
| `hint{variant 1}.reversalSource` | string, nullable | Yes | Allowed values: `staff`, `tenant_api`, `appeal_overturned`, `detection_dismissed`, `detection_overturned`, `detection_suppressed`, `null`. |
| `hint{variant 1}.reversalReasonCode` | string, nullable | Yes | Allowed values: `handled`, `false_positive`, `account_verified`, `policy_exception`, `other`, `null`. |

### Example request

**curl**

```bash
curl "https://pulse.noxtica.com/pulse/v1/tenant/decisions/example-id" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

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

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

## Withdraw an active response hint {#pulseTenantReverseDecision}

`POST /pulse/v1/tenant/decisions/{id}/reverse`

Scope `write:pulse`. The tenant back end marks the decision's ACTIVE hint as handled or withdrawn, with a closed `reasonCode`. The reversal is an append-only audit event (actor = the API key id) and is announced by the `pulse.response.v1` webhook (transition `reversed`). Idempotent: an already reversed hint answers `200` and writes nothing. A decision without an active hint (a dry run, a suppressed action, an expired hint) is `409 hint_not_active`.

**Authentication:** Bearer token (`tenantKey`), scope `write:pulse`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes | Pattern `^pact_[0-7][0-9A-HJKMNP-TV-Z]{25}$`. |

### Request body

Content type `application/json`, required.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `reasonCode` | string | Yes | Allowed values: `handled`, `false_positive`, `account_verified`, `policy_exception`, `other`. |
| `note` | string | No | At most 500 characters. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The decision with `hint.state` = `reversed`. |  | `application/json` TenantDecision |
| `400` | `invalid_request`. | `invalid_request` | `application/json` ApiError |
| `401` | `missing_api_key` or `invalid_api_key`. | `invalid_api_key`, `missing_api_key` | `application/json` ApiError |
| `403` | `insufficient_scope` (needs `write:pulse`) or `tenant_not_armed`. | `insufficient_scope`, `tenant_not_armed` | `application/json` ApiError |
| `404` | `decision_not_found`. | `decision_not_found` | `application/json` ApiError |
| `409` | `hint_not_active`. | `hint_not_active` | `application/json` ApiError |
| `415` | Failure in the platform `apiError` envelope. |  | `application/json` ApiError |
| `429` | Too many attempts. |  | `application/json` ApiError |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `429` | `Retry-After` | integer | Seconds until the window resets. |

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

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `schemaVersion` | const | Yes | Always `pulse.tenant.decision/v1`. |
| `id` | string | Yes | Pattern `^pact_[0-7][0-9A-HJKMNP-TV-Z]{25}$`. |
| `appId` | string | Yes |  |
| `subject` | object | Yes |  |
| `subject.psub` | string | Yes |  |
| `subject.deviceRef` | string | Yes |  |
| `action` | string | Yes | The closed local-action allow-list of the response executor. Nothing else is representable. Allowed values: `notify_user`, `suspend_session_hint`. |
| `outcome` | string | Yes | The outcome of record after the server's gates (a report the server downgraded reads `suppressed_kill_switch`, `suppressed_requires_human` or `dry_run`). Allowed values: `executed`, `dry_run`, `suppressed_rate_limit`, `suppressed_cooldown`, `suppressed_kill_switch`, `suppressed_requires_human`. |
| `reason` | string | Yes |  |
| `reasonCode` | string | Yes |  |
| `severity` | string | Yes | Allowed values: `low`, `medium`, `high`, `critical`. |
| `evidenceTier` | string | Yes | Allowed values: `t0`, `t1`. |
| `ruleId` | string | Yes |  |
| `ladderId` | string | Yes |  |
| `bundleRevision` | integer | Yes |  |
| `observedAt` | integer (int64) | Yes |  |
| `recordedAt` | integer (int64) | Yes |  |
| `detection` | one of object or any, nullable | Yes | The detection the decision answers; `status` and `appealState` are `null` while its event has not been ingested. |
| `detection{variant 1}` | object | No |  |
| `detection{variant 1}.id` | string | Yes |  |
| `detection{variant 1}.status` | string, nullable | Yes | Allowed values: `open`, `under_review`, `confirmed`, `dismissed`, `overturned`, `suppressed`, `null`. |
| `detection{variant 1}.appealState` | string, nullable | Yes | Allowed values: `none`, `open`, `upheld`, `overturned`, `withdrawn`, `null`. |
| `hint` | one of object or any, nullable | Yes | `null` when the decision produced no hint. |
| `hint{variant 1}` | object | No |  |
| `hint{variant 1}.state` | string | Yes | Allowed values: `active`, `expired`, `reversed`. |
| `hint{variant 1}.issuedAt` | integer (int64) | Yes | When the hint began: `min(observed_at, recorded_at)` — never later than the server saw the report. Equals `responseHints[].issuedAt` of the subject status. |
| `hint{variant 1}.expiresAt` | integer (int64) | Yes |  |
| `hint{variant 1}.reversedAt` | integer (int64), nullable | Yes |  |
| `hint{variant 1}.reversalSource` | string, nullable | Yes | Allowed values: `staff`, `tenant_api`, `appeal_overturned`, `detection_dismissed`, `detection_overturned`, `detection_suppressed`, `null`. |
| `hint{variant 1}.reversalReasonCode` | string, nullable | Yes | Allowed values: `handled`, `false_positive`, `account_verified`, `policy_exception`, `other`, `null`. |

### Example request

**curl**

```bash
curl -X POST "https://pulse.noxtica.com/pulse/v1/tenant/decisions/example-id/reverse" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reasonCode": "handled"
}'
```

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

```js
const response = await fetch('https://pulse.noxtica.com/pulse/v1/tenant/decisions/example-id/reverse', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    reasonCode: 'handled',
  }),
});
console.log(response.status, await response.text());
```

**Python** (standard library)

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

request = urllib.request.Request(
    "https://pulse.noxtica.com/pulse/v1/tenant/decisions/example-id/reverse",
    data=json.dumps({
        "reasonCode": "handled",
    }).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 one-time link code for one of the tenant's own users {#pulseTenantCreateLinkCode}

`POST /pulse/v1/tenant/link-codes`

Server-to-server, scope `write:pulse`. The tenant backend (a launcher, a game server) asks for a code for ITS user `externalUserId` in one of its applications and hands the returned `url` to that person. The code is shown once, valid 10 minutes, single use; only its SHA-256 is stored. The person opens the URL signed in to their Pulse account, sees the tenant, the application, the masked user id and the data the tenant will see, and approves (the link is the consent receipt) or denies. At most 5 unconsumed codes exist per (application, `externalUserId`) (`429 too_many_outstanding_codes`, `Retry-After: 600`); 30 codes per key per minute and 600 per tenant per hour (`429 rate_limited`). A user id that already has an active link in that application is `409 external_user_already_linked` (unlink it first). `Cache-Control: no-store`.

**Authentication:** Bearer token (`tenantKey`), scope `write:pulse`.

### Request body

Content type `application/json`, required.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `externalUserId` | string | Yes | The tenant's own user id. No leading/trailing whitespace, no control characters. At least 1 character, at most 256 characters. |
| `appId` | string | Yes | An ACTIVE application of this tenant. Pattern `^[A-Za-z0-9_.:-]{1,64}$`. |
| `locale` | string | No | BCP 47 tag appended to the `url` so the consent screen opens in the person's language. At most 35 characters. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `201` | The code (shown ONCE). |  | `application/json` TenantLinkCode |
| `400` | `invalid_parameter` / `missing_parameter` (empty, padded, control-character or over-long `externalUserId`; malformed `appId` or `locale`), or a non-JSON body. | `invalid_parameter` | `application/json` ApiError |
| `401` | `missing_api_key` or `invalid_api_key` (malformed, unknown, revoked, or a device token). `WWW-Authenticate: Bearer realm="pulse-tenant"`. | `invalid_api_key`, `missing_api_key` | `application/json` ApiError |
| `403` | `insufficient_scope` (the key lacks the scope the operation names) or `tenant_not_armed`. | `insufficient_scope`, `tenant_not_armed` | `application/json` ApiError |
| `404` | `application_not_found` — not an active application of this tenant. | `application_not_found` | `application/json` ApiError |
| `409` | `external_user_already_linked`. | `external_user_already_linked` | `application/json` ApiError |
| `415` | Failure in the platform `apiError` envelope. |  | `application/json` ApiError |
| `429` | Too many attempts. | `rate_limited`, `too_many_outstanding_codes` | `application/json` ApiError |
| `503` | `tenant_auth_unavailable` or `rate_limiter_unavailable` (fail closed). | `rate_limiter_unavailable`, `tenant_auth_unavailable` | `application/json` ApiError |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `201` | `Cache-Control` | const |  |
| `429` | `Retry-After` | integer | Seconds until the window resets. |

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

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `code` | string | Yes | Shown once; single use. Pattern `^pl_[A-Za-z0-9_-]{32}$`. |
| `expiresAt` | integer (int64) | Yes | Epoch ms; 10 minutes after issue. |
| `url` | string (uri) | Yes | `{app origin}/link?code=…[&locale=…]` — the page the person opens. |

### Example request

**curl**

```bash
curl -X POST "https://pulse.noxtica.com/pulse/v1/tenant/link-codes" \
  -H "Authorization: Bearer $NOXTICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "externalUserId": "example-externalUserId",
  "appId": "example-appId"
}'
```

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

```js
const response = await fetch('https://pulse.noxtica.com/pulse/v1/tenant/link-codes', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOXTICA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    externalUserId: 'example-externalUserId',
    appId: 'example-appId',
  }),
});
console.log(response.status, await response.text());
```

**Python** (standard library)

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

request = urllib.request.Request(
    "https://pulse.noxtica.com/pulse/v1/tenant/link-codes",
    data=json.dumps({
        "externalUserId": "example-externalUserId",
        "appId": "example-appId",
    }).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 one link of the calling tenant {#pulseTenantGetLink}

`GET /pulse/v1/tenant/links/{subject}`

Scope `read:pulse`. `subject` is the pairwise `plnk_…` (resolves in EVERY state, so an unlink is observable as `state: unlinked` with `unlinkedBy`) or the tenant's own `externalUserId` (resolves only through its ACTIVE link; `409 ambiguous_subject` when it is linked in several applications unless `?appId=` names one). Another tenant's link, an unknown subject and an unlinked `externalUserId` are the same `404 subject_not_found`. The answer never carries an account id, e-mail, psub or device fact. `Cache-Control: no-store`.

**Authentication:** Bearer token (`tenantKey`), scope `read:pulse`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `subject` | path | string | Yes | The pairwise `plnk_…` subject or the tenant's own `externalUserId` (1..256 characters, percent-encoded). At least 1 character, at most 256 characters. |
| `appId` | query | string | No | Narrows an `externalUserId` that is linked in several applications of the tenant (else `409 ambiguous_subject`). Pattern `^[A-Za-z0-9_.:-]{1,64}$`. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The link. |  | `application/json` TenantLink |
| `400` | `invalid_subject` or `invalid_parameter` (malformed `appId`). | `invalid_parameter`, `invalid_subject` | `application/json` ApiError |
| `401` | `missing_api_key` or `invalid_api_key` (malformed, unknown, revoked, or a device token). `WWW-Authenticate: Bearer realm="pulse-tenant"`. | `invalid_api_key`, `missing_api_key` | `application/json` ApiError |
| `403` | `insufficient_scope` (the key lacks the scope the operation names) or `tenant_not_armed`. | `insufficient_scope`, `tenant_not_armed` | `application/json` ApiError |
| `404` | `subject_not_found`. | `subject_not_found` | `application/json` ApiError |
| `409` | `ambiguous_subject`. | `ambiguous_subject` | `application/json` ApiError |
| `429` | Too many attempts. |  | `application/json` ApiError |
| `503` | `tenant_auth_unavailable` or `rate_limiter_unavailable` (fail closed). | `rate_limiter_unavailable`, `tenant_auth_unavailable` | `application/json` ApiError |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `200` | `Cache-Control` | const |  |
| `429` | `Retry-After` | integer | Seconds until the window resets. |

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

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `schemaVersion` | const | Yes | Always `pulse.tenant.link/v1`. |
| `subject` | string | Yes | The pairwise subject: random per link, never derived from the account. Pattern `^plnk_[0-9A-HJKMNP-TV-Z]{26}$`. |
| `externalUserId` | string | Yes |  |
| `applicationId` | string | Yes |  |
| `state` | string | Yes | Allowed values: `active`, `unlinked`. |
| `scopes` | array of string | Yes |  |
| `linkedAt` | integer (int64) | Yes |  |
| `unlinkedAt` | integer (int64), nullable | Yes |  |
| `unlinkedBy` | one of LinkUnlinkedBy or any, nullable | Yes |  |

### Example request

**curl**

```bash
curl "https://pulse.noxtica.com/pulse/v1/tenant/links/plnk_01M4GW6CR553W8AAWXEG17RDJW" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

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

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

## Unlink a player (tenant side) {#pulseTenantUnlink}

`DELETE /pulse/v1/tenant/links/{subject}`

Scope `write:pulse`. Ends the link (`unlinkedBy: tenant`): the player's enrolments of that application lose the binding, the link is audited, and a `pulse.enrolment.linked.v1` webhook (`transition: unlinked`) is queued in the same transaction. Idempotent: an already unlinked link answers `200` with `changed: false` and writes nothing. Same subject forms, `?appId=` rule and `404 subject_not_found` as `GET`. The player can link again with a new code; that creates a NEW `plnk_…` subject.

**Authentication:** Bearer token (`tenantKey`), scope `write:pulse`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `subject` | path | string | Yes | The pairwise `plnk_…` subject or the tenant's own `externalUserId` (1..256 characters, percent-encoded). At least 1 character, at most 256 characters. |
| `appId` | query | string | No | Narrows an `externalUserId` that is linked in several applications of the tenant (else `409 ambiguous_subject`). Pattern `^[A-Za-z0-9_.:-]{1,64}$`. |

### Responses

| Status | Meaning | Error codes | Content |
| --- | --- | --- | --- |
| `200` | The link after the call. |  | `application/json` TenantLinkUnlinked |
| `400` | `invalid_subject` or `invalid_parameter`. | `invalid_parameter`, `invalid_subject` | `application/json` ApiError |
| `401` | `missing_api_key` or `invalid_api_key` (malformed, unknown, revoked, or a device token). `WWW-Authenticate: Bearer realm="pulse-tenant"`. | `invalid_api_key`, `missing_api_key` | `application/json` ApiError |
| `403` | `insufficient_scope` (the key lacks the scope the operation names) or `tenant_not_armed`. | `insufficient_scope`, `tenant_not_armed` | `application/json` ApiError |
| `404` | `subject_not_found`. | `subject_not_found` | `application/json` ApiError |
| `409` | `ambiguous_subject`. | `ambiguous_subject` | `application/json` ApiError |
| `429` | Too many attempts. |  | `application/json` ApiError |
| `503` | `tenant_auth_unavailable` or `rate_limiter_unavailable` (fail closed). | `rate_limiter_unavailable`, `tenant_auth_unavailable` | `application/json` ApiError |

**Response headers**

| Status | Header | Type | Description |
| --- | --- | --- | --- |
| `200` | `Cache-Control` | const |  |
| `429` | `Retry-After` | integer | Seconds until the window resets. |

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

| Field | Type | Always returned | Description |
| --- | --- | --- | --- |
| `schemaVersion` | const | Yes | Always `pulse.tenant.link/v1`. |
| `subject` | string | Yes | The pairwise subject: random per link, never derived from the account. Pattern `^plnk_[0-9A-HJKMNP-TV-Z]{26}$`. |
| `externalUserId` | string | Yes |  |
| `applicationId` | string | Yes |  |
| `state` | string | Yes | Allowed values: `active`, `unlinked`. |
| `scopes` | array of string | Yes |  |
| `linkedAt` | integer (int64) | Yes |  |
| `unlinkedAt` | integer (int64), nullable | Yes |  |
| `unlinkedBy` | one of LinkUnlinkedBy or any, nullable | Yes |  |
| `changed` | boolean | Yes | `false` when the link was already unlinked (nothing written). |

### Example request

**curl**

```bash
curl -X DELETE "https://pulse.noxtica.com/pulse/v1/tenant/links/plnk_01M4GW6CR553W8AAWXEG17RDJW" \
  -H "Authorization: Bearer $NOXTICA_API_KEY"
```

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

```js
const response = await fetch('https://pulse.noxtica.com/pulse/v1/tenant/links/plnk_01M4GW6CR553W8AAWXEG17RDJW', {
  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://pulse.noxtica.com/pulse/v1/tenant/links/plnk_01M4GW6CR553W8AAWXEG17RDJW",
    headers={
        "Authorization": f"Bearer {os.environ['NOXTICA_API_KEY']}",
    },
    method="DELETE",
)
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`: `pulse.yaml` version `0.9.0-2026-10-27` (SHA-256 `95196a58bb5b`). Do not edit it by hand._
