On this page

Pulse tenant API

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

EnvironmentBase URL
Production (not yet deployed)https://pulse.noxtica.com

Credential: tenantKey. See Authentication.

How this page is organisedBar length shows each section’s share of the text. Select a section to jump to it.

Tenant

7 operations

Operations in Pulse tenant API

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

NameInTypeRequiredDescription
subjectpathstringYesAt least 1 character, at most 256 characters.
appIdquerystringNoNarrows an externalUserId that is linked in several applications of the tenant (else 409 ambiguous_subject). Pattern ^[A-Za-z0-9_.:-]{1,64}$.

Responses

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

Response headers

StatusHeaderTypeDescription
200Cache-Controlconst
429Retry-AfterintegerSeconds until the window resets.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
schemaVersionconstYesAlways pulse.tenant.subject-status/v1.
scopesarray of stringNoPresent only for a subject resolved through a link: the scopes the player granted.
subjectstringYesThe subject exactly as asked.
enrolledbooleanNoAt least one active enrolment.
deviceAttestationLevelone of string or any, nullableNoHighest attestation level among the active enrolments; null when not enrolled.
attestationone of TenantAttestation or any, nullableNoDET-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)NoDET-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}.levelstringYesAllowed values: L0, L1, L2, L3.
attestation{TenantAttestation}.reasonCodesarray of stringYes
attestation{TenantAttestation}.attestedAtone of integer (int64) or any, nullableYes
attestation{TenantAttestation}.expiresAtone of integer (int64) or any, nullableYes
attestation{TenantAttestation}.policyRevisionone of integer or any, nullableYes
lastSeenone of integer (int64) or any, nullableNoLatest control-plane activity (epoch ms) of any enrolment of the subject, null when never.
openSignalsarray of TenantOpenSignalNoAt most 50 items.
openSignals[]object (TenantOpenSignal)No
openSignals[].reasonCodestringYesPattern ^[A-Z]{3}-[0-9]{3}$.
openSignals[].severitystringYesAllowed values: low, medium, high, critical.
openSignals[].tierstringYesAllowed values: t0, t1, t2.
openSignals[].statusstringYesAllowed values: open, under_review.
openSignals[].detectedAtinteger (int64)Yes
responseHintsarray of TenantResponseHintNoAdditive. 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[].decisionIdstringYesThe pact_… id of the decision (GET /pulse/v1/tenant/decisions/{id}).
responseHints[].actionconstYesAlways suspend_session_hint.
responseHints[].reasonCodestringYes
responseHints[].severitystringYesAllowed values: low, medium, high, critical.
responseHints[].issuedAtinteger (int64)YesWhen the hint began: min(observed_at, recorded_at) of the decision (equals hint.issuedAt of GET /pulse/v1/tenant/decisions/{id}).
responseHints[].expiresAtinteger (int64)Yes
revokedAtinteger (int64)NoPresent only when enrolled is false.
reasonCodestringNoPresent 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

NameInTypeRequiredDescription
app_idquerystringNoPattern ^[a-z0-9][a-z0-9-]{0,62}$.
device_refquerystringNoThe 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}$.
severityquerystringNoAllowed values: low, medium, high, critical.
subjectquerystringNoAt least 1 character, at most 256 characters.
outcomequerystringNoAllowed values: executed, dry_run, suppressed_rate_limit, suppressed_cooldown, suppressed_kill_switch, suppressed_requires_human.
actionquerystringNoThe closed local-action allow-list of the response executor. Nothing else is representable. Allowed values: notify_user, suspend_session_hint.
reason_codequerystringNoPattern ^[A-Z]{3}-[0-9]{3}$.
rule_idquerystringNoAt most 64 characters.
hint_statequerystringNoAllowed values: active, expired, reversed, none.
detection_idquerystringNoPattern ^pdet_[0-9A-HJKMNP-TV-Z]{26}$.
fromqueryinteger (int64)NoEpoch ms (inclusive) of the device decision. Minimum 1.
toqueryinteger (int64)NoEpoch ms (exclusive). Minimum 1.
limitqueryintegerNoMinimum 1, maximum 200. Default 50.
cursorquerystringNoAt most 96 characters.

Responses

StatusMeaningError codesContent
200One page of decisions.application/json TenantDecisionList
400invalid_request (a malformed filter) or invalid_cursor.invalid_cursor, invalid_requestapplication/json ApiError
401missing_api_key or invalid_api_key (a device token is never accepted).invalid_api_key, missing_api_keyapplication/json ApiError
403insufficient_scope, tenant_not_armed or scope_not_granted.insufficient_scope, scope_not_granted, tenant_not_armedapplication/json ApiError
404subject_not_found.subject_not_foundapplication/json ApiError
429Too many attempts.application/json ApiError

Response headers

StatusHeaderTypeDescription
200Cache-Controlconst
429Retry-AfterintegerSeconds until the window resets.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
schemaVersionconstYesAlways pulse.tenant.decisions/v1.
decisionsarray of TenantDecisionYes
decisions[]object (TenantDecision)No
decisions[].schemaVersionconstYesAlways pulse.tenant.decision/v1.
decisions[].idstringYesPattern ^pact_[0-7][0-9A-HJKMNP-TV-Z]{25}$.
decisions[].appIdstringYes
decisions[].subjectobjectYes
decisions[].subject.psubstringYes
decisions[].subject.deviceRefstringYes
decisions[].actionstringYesThe closed local-action allow-list of the response executor. Nothing else is representable. Allowed values: notify_user, suspend_session_hint.
decisions[].outcomestringYesThe 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[].reasonstringYes
decisions[].reasonCodestringYes
decisions[].severitystringYesAllowed values: low, medium, high, critical.
decisions[].evidenceTierstringYesAllowed values: t0, t1.
decisions[].ruleIdstringYes
decisions[].ladderIdstringYes
decisions[].bundleRevisionintegerYes
decisions[].observedAtinteger (int64)Yes
decisions[].recordedAtinteger (int64)Yes
decisions[].detectionone of object or any, nullableYesThe detection the decision answers; status and appealState are null while its event has not been ingested.
decisions[].detection{variant 1}objectNo
decisions[].detection{variant 1}.idstringYes
decisions[].detection{variant 1}.statusstring, nullableYesAllowed values: open, under_review, confirmed, dismissed, overturned, suppressed, null.
decisions[].detection{variant 1}.appealStatestring, nullableYesAllowed values: none, open, upheld, overturned, withdrawn, null.
decisions[].hintone of object or any, nullableYesnull when the decision produced no hint.
decisions[].hint{variant 1}objectNo
decisions[].hint{variant 1}.statestringYesAllowed values: active, expired, reversed.
decisions[].hint{variant 1}.issuedAtinteger (int64)YesWhen 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}.expiresAtinteger (int64)Yes
decisions[].hint{variant 1}.reversedAtinteger (int64), nullableYes
decisions[].hint{variant 1}.reversalSourcestring, nullableYesAllowed values: staff, tenant_api, appeal_overturned, detection_dismissed, detection_overturned, detection_suppressed, null.
decisions[].hint{variant 1}.reversalReasonCodestring, nullableYesAllowed values: handled, false_positive, account_verified, policy_exception, other, null.
nextCursorstring, nullableYesOpaque; 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

NameInTypeRequiredDescription
idpathstringYesPattern ^pact_[0-7][0-9A-HJKMNP-TV-Z]{25}$.

Responses

StatusMeaningError codesContent
200The decision.application/json TenantDecision
401missing_api_key or invalid_api_key.invalid_api_key, missing_api_keyapplication/json ApiError
403insufficient_scope or tenant_not_armed.insufficient_scope, tenant_not_armedapplication/json ApiError
404decision_not_found.decision_not_foundapplication/json ApiError
429Too many attempts.application/json ApiError

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the window resets.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
schemaVersionconstYesAlways pulse.tenant.decision/v1.
idstringYesPattern ^pact_[0-7][0-9A-HJKMNP-TV-Z]{25}$.
appIdstringYes
subjectobjectYes
subject.psubstringYes
subject.deviceRefstringYes
actionstringYesThe closed local-action allow-list of the response executor. Nothing else is representable. Allowed values: notify_user, suspend_session_hint.
outcomestringYesThe 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.
reasonstringYes
reasonCodestringYes
severitystringYesAllowed values: low, medium, high, critical.
evidenceTierstringYesAllowed values: t0, t1.
ruleIdstringYes
ladderIdstringYes
bundleRevisionintegerYes
observedAtinteger (int64)Yes
recordedAtinteger (int64)Yes
detectionone of object or any, nullableYesThe detection the decision answers; status and appealState are null while its event has not been ingested.
detection{variant 1}objectNo
detection{variant 1}.idstringYes
detection{variant 1}.statusstring, nullableYesAllowed values: open, under_review, confirmed, dismissed, overturned, suppressed, null.
detection{variant 1}.appealStatestring, nullableYesAllowed values: none, open, upheld, overturned, withdrawn, null.
hintone of object or any, nullableYesnull when the decision produced no hint.
hint{variant 1}objectNo
hint{variant 1}.statestringYesAllowed values: active, expired, reversed.
hint{variant 1}.issuedAtinteger (int64)YesWhen 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}.expiresAtinteger (int64)Yes
hint{variant 1}.reversedAtinteger (int64), nullableYes
hint{variant 1}.reversalSourcestring, nullableYesAllowed values: staff, tenant_api, appeal_overturned, detection_dismissed, detection_overturned, detection_suppressed, null.
hint{variant 1}.reversalReasonCodestring, nullableYesAllowed 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

NameInTypeRequiredDescription
idpathstringYesPattern ^pact_[0-7][0-9A-HJKMNP-TV-Z]{25}$.

Request body

Content type application/json, required.

FieldTypeRequiredDescription
reasonCodestringYesAllowed values: handled, false_positive, account_verified, policy_exception, other.
notestringNoAt most 500 characters.

Responses

StatusMeaningError codesContent
200The decision with hint.state = reversed.application/json TenantDecision
400invalid_request.invalid_requestapplication/json ApiError
401missing_api_key or invalid_api_key.invalid_api_key, missing_api_keyapplication/json ApiError
403insufficient_scope (needs write:pulse) or tenant_not_armed.insufficient_scope, tenant_not_armedapplication/json ApiError
404decision_not_found.decision_not_foundapplication/json ApiError
409hint_not_active.hint_not_activeapplication/json ApiError
415Failure in the platform apiError envelope.application/json ApiError
429Too many attempts.application/json ApiError

Response headers

StatusHeaderTypeDescription
429Retry-AfterintegerSeconds until the window resets.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
schemaVersionconstYesAlways pulse.tenant.decision/v1.
idstringYesPattern ^pact_[0-7][0-9A-HJKMNP-TV-Z]{25}$.
appIdstringYes
subjectobjectYes
subject.psubstringYes
subject.deviceRefstringYes
actionstringYesThe closed local-action allow-list of the response executor. Nothing else is representable. Allowed values: notify_user, suspend_session_hint.
outcomestringYesThe 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.
reasonstringYes
reasonCodestringYes
severitystringYesAllowed values: low, medium, high, critical.
evidenceTierstringYesAllowed values: t0, t1.
ruleIdstringYes
ladderIdstringYes
bundleRevisionintegerYes
observedAtinteger (int64)Yes
recordedAtinteger (int64)Yes
detectionone of object or any, nullableYesThe detection the decision answers; status and appealState are null while its event has not been ingested.
detection{variant 1}objectNo
detection{variant 1}.idstringYes
detection{variant 1}.statusstring, nullableYesAllowed values: open, under_review, confirmed, dismissed, overturned, suppressed, null.
detection{variant 1}.appealStatestring, nullableYesAllowed values: none, open, upheld, overturned, withdrawn, null.
hintone of object or any, nullableYesnull when the decision produced no hint.
hint{variant 1}objectNo
hint{variant 1}.statestringYesAllowed values: active, expired, reversed.
hint{variant 1}.issuedAtinteger (int64)YesWhen 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}.expiresAtinteger (int64)Yes
hint{variant 1}.reversedAtinteger (int64), nullableYes
hint{variant 1}.reversalSourcestring, nullableYesAllowed values: staff, tenant_api, appeal_overturned, detection_dismissed, detection_overturned, detection_suppressed, null.
hint{variant 1}.reversalReasonCodestring, nullableYesAllowed 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.

FieldTypeRequiredDescription
externalUserIdstringYesThe tenant’s own user id. No leading/trailing whitespace, no control characters. At least 1 character, at most 256 characters.
appIdstringYesAn ACTIVE application of this tenant. Pattern ^[A-Za-z0-9_.:-]{1,64}$.
localestringNoBCP 47 tag appended to the url so the consent screen opens in the person’s language. At most 35 characters.

Responses

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

Response headers

StatusHeaderTypeDescription
201Cache-Controlconst
429Retry-AfterintegerSeconds until the window resets.

Response fields (201, application/json)

FieldTypeAlways returnedDescription
codestringYesShown once; single use. Pattern ^pl_[A-Za-z0-9_-]{32}$.
expiresAtinteger (int64)YesEpoch ms; 10 minutes after issue.
urlstring (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())

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

NameInTypeRequiredDescription
subjectpathstringYesThe pairwise plnk_… subject or the tenant’s own externalUserId (1..256 characters, percent-encoded). At least 1 character, at most 256 characters.
appIdquerystringNoNarrows an externalUserId that is linked in several applications of the tenant (else 409 ambiguous_subject). Pattern ^[A-Za-z0-9_.:-]{1,64}$.

Responses

StatusMeaningError codesContent
200The link.application/json TenantLink
400invalid_subject or invalid_parameter (malformed appId).invalid_parameter, invalid_subjectapplication/json ApiError
401missing_api_key or invalid_api_key (malformed, unknown, revoked, or a device token). WWW-Authenticate: Bearer realm="pulse-tenant".invalid_api_key, missing_api_keyapplication/json ApiError
403insufficient_scope (the key lacks the scope the operation names) or tenant_not_armed.insufficient_scope, tenant_not_armedapplication/json ApiError
404subject_not_found.subject_not_foundapplication/json ApiError
409ambiguous_subject.ambiguous_subjectapplication/json ApiError
429Too many attempts.application/json ApiError
503tenant_auth_unavailable or rate_limiter_unavailable (fail closed).rate_limiter_unavailable, tenant_auth_unavailableapplication/json ApiError

Response headers

StatusHeaderTypeDescription
200Cache-Controlconst
429Retry-AfterintegerSeconds until the window resets.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
schemaVersionconstYesAlways pulse.tenant.link/v1.
subjectstringYesThe pairwise subject: random per link, never derived from the account. Pattern ^plnk_[0-9A-HJKMNP-TV-Z]{26}$.
externalUserIdstringYes
applicationIdstringYes
statestringYesAllowed values: active, unlinked.
scopesarray of stringYes
linkedAtinteger (int64)Yes
unlinkedAtinteger (int64), nullableYes
unlinkedByone of LinkUnlinkedBy or any, nullableYes

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())

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

NameInTypeRequiredDescription
subjectpathstringYesThe pairwise plnk_… subject or the tenant’s own externalUserId (1..256 characters, percent-encoded). At least 1 character, at most 256 characters.
appIdquerystringNoNarrows an externalUserId that is linked in several applications of the tenant (else 409 ambiguous_subject). Pattern ^[A-Za-z0-9_.:-]{1,64}$.

Responses

StatusMeaningError codesContent
200The link after the call.application/json TenantLinkUnlinked
400invalid_subject or invalid_parameter.invalid_parameter, invalid_subjectapplication/json ApiError
401missing_api_key or invalid_api_key (malformed, unknown, revoked, or a device token). WWW-Authenticate: Bearer realm="pulse-tenant".invalid_api_key, missing_api_keyapplication/json ApiError
403insufficient_scope (the key lacks the scope the operation names) or tenant_not_armed.insufficient_scope, tenant_not_armedapplication/json ApiError
404subject_not_found.subject_not_foundapplication/json ApiError
409ambiguous_subject.ambiguous_subjectapplication/json ApiError
429Too many attempts.application/json ApiError
503tenant_auth_unavailable or rate_limiter_unavailable (fail closed).rate_limiter_unavailable, tenant_auth_unavailableapplication/json ApiError

Response headers

StatusHeaderTypeDescription
200Cache-Controlconst
429Retry-AfterintegerSeconds until the window resets.

Response fields (200, application/json)

FieldTypeAlways returnedDescription
schemaVersionconstYesAlways pulse.tenant.link/v1.
subjectstringYesThe pairwise subject: random per link, never derived from the account. Pattern ^plnk_[0-9A-HJKMNP-TV-Z]{26}$.
externalUserIdstringYes
applicationIdstringYes
statestringYesAllowed values: active, unlinked.
scopesarray of stringYes
linkedAtinteger (int64)Yes
unlinkedAtinteger (int64), nullableYes
unlinkedByone of LinkUnlinkedBy or any, nullableYes
changedbooleanYesfalse 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.