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.
- Status of one player (pseudonymous subject) of the calling tenant
- List the tenant’s response decisions (audit), newest first
- One response decision
- Withdraw an active response hint
- Issue a one-time link code for one of the tenant’s own users
- Read one link of the calling tenant
- Unlink a player (tenant side)
Tenant
7 operations
- GET /pulse/v1/tenant/subjects/{subject} Status of one player (pseudonymous subject) of the calling tenant
- GET /pulse/v1/tenant/decisions List the tenant's response decisions (audit), newest first
- GET /pulse/v1/tenant/decisions/{id} One response decision
- POST /pulse/v1/tenant/decisions/{id}/reverse Withdraw an active response hint
- POST /pulse/v1/tenant/link-codes Issue a one-time link code for one of the tenant's own users
- GET /pulse/v1/tenant/links/{subject} Read one link of the calling tenant
- DELETE /pulse/v1/tenant/links/{subject} Unlink a player (tenant side)
Status of one player (pseudonymous subject) of the calling tenant
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
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)
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)
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
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
curl "https://pulse.noxtica.com/pulse/v1/tenant/decisions" \
-H "Authorization: Bearer $NOXTICA_API_KEY"
Node.js (global fetch, Node 18 or later)
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)
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
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
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)
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)
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
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
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)
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)
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
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
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)
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)
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
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
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)
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)
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)
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
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)
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)
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.