Skip to main content
Docs navigation

Entity & Consent API

The identity spine: anonymous, consent-gated visitor identities your site mints, tracks, and erases — all on your own WordPress install.

An entity is a persistent, anonymous identity for someone engaging with your events — playing bingo, voting, checking in — created only when they explicitly consent. No name or email is required. The entity can later resolve into a full member, carrying its history with it. Everything lives in your own site's nwp_* tables.

Public endpoints, token-scoped access

Granting consent needs no authentication — it is the moment tracking is allowed to begin. Every other entity endpoint requires the X-NWP-Entity-Token header carrying the token minted at consent time, so an entity can only ever read or erase itself.

The entity token

The token has the form <entity_key>.<hmac> — a random key signed with a per-site secret. It is returned once, in the consent response, and never again: the server stores only its SHA-256 hash. If the visitor's browser loses it, there is no recovery — they simply consent again and get a fresh entity. Clients present it on every request:

bash
X-NWP-Entity-Token: 9f1c2ab34d5e6f7089a1b2c3d4e5f601.7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f70819aabbccd

A missing header returns 401 entity_token_missing; a forged or unknown token returns 401 entity_token_invalid.

The public id

Alongside the secret token, every entity carries a portable, non-secret public id: nwe_ followed by a 26-character Crockford base32 ULID (30 chars total, e.g. nwe_01KXBQKEMBAW4EXM1JD5D7B9YD). It is time-sortable and safe to expose in exports, QR codes, and CRM syncs — the token stays the credential. The globally unique coordinate of any entity is the pair (platform_id, public_id).

Consent scopes

Consent is granted per scope. The built-in vocabulary (extensible via the nwp_consent_scopes filter):

ScopeWhat it permits
engagementTracking event engagement (check-ins, games, votes). Governs whether new activity accrues.
pushPush notifications to this device.
email_marketingMarketing emails.
sms_marketingMarketing text messages.
crm_syncSyncing contact details and engagement history to the operator's CRM.

POST /entity/consent

Records consent, mints the entity, and returns the token. scope defaults to ["engagement"] when omitted; source is a free-form label for where consent was captured (e.g. bingo_join) and lands on the consent ledger for the audit trail.

bash
$ curl -X POST https://members.example.com/wp-json/nwp/v1/entity/consent \$   -H "Content-Type: application/json" \$   -d '{"scope": ["engagement", "push"], "source": "bingo_join"}'

The 201 response:

json
{
  "success": true,
  "data": {
    "token": "9f1c2ab34d5e6f7089a1b2c3d4e5f601.7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f70819aabbccd",
    "entity_id": 6,
    "entity_public_id": "nwe_01KXBQKEMBAW4EXM1JD5D7B9YD",
    "status": "anonymous",
    "consent": {
      "granted_at": "2026-07-12 17:57:03",
      "scope": ["engagement", "push"],
      "source": "bingo_join",
      "history": [
        { "event": "granted", "at": "2026-07-12 17:57:03", "scope": ["engagement"], "source": "bingo_join" },
        { "event": "granted", "at": "2026-07-12 17:57:03", "scope": ["push"], "source": "bingo_join" }
      ]
    },
    "policy": {
      "engagement": { "version": 1, "copy": "I agree to have my event engagement (check-ins, games, votes) tracked..." },
      "push": { "version": 1, "copy": "I agree to receive push notifications from this organization on this device..." }
    },
    "gpc": false
  }
}

The token appears exactly once

data.token is shown in this response and nowhere else — the server keeps only its hash. Persist it client-side (e.g. localStorage) immediately.

policy echoes the exact consent copy and version the visitor agreed to, per scope — see Consent & your audience data for how versioning works.

Errors

StatusCodeWhen
400entity_invalid_scopeAny scope key is unrecognized. data.allowed_scopes lists the valid ones. Rejected before the throttle, so bad payloads never consume quota.
429entity_mint_throttledMore than 10 mints per 300 seconds per IP. data.retry_after carries the window in seconds.
json
{
  "code": "entity_invalid_scope",
  "message": "Unknown consent scope(s): telepathy. Allowed scopes: engagement, push, email_marketing, sms_marketing, crm_sync.",
  "data": {
    "status": 400,
    "allowed_scopes": ["engagement", "push", "email_marketing", "sms_marketing", "crm_sync"]
  }
}

Global Privacy Control

A browser asserting GPC (Sec-GPC: 1) is honored automatically: crm_sync is stripped from the requested scopes before minting, the opt-out is recorded on the consent ledger as a first-class event, and the response carries "gpc": true so your frontend can suppress further consent prompts for that scope.

POST /entity/consent/revoke

Revokes all active scopes. New tracking stops immediately; the entity remains as an auditable stub. Requires the token header.

bash
$ curl -X POST https://members.example.com/wp-json/nwp/v1/entity/consent/revoke \$   -H "X-NWP-Entity-Token: $ENTITY_TOKEN"
json
{
  "success": true,
  "data": { "revoked": true, "entity_id": 6 }
}

GET /entity/me

The entity's own summary: status, consent record, active policy, and its 25 most recent activity events. member_id is null until the entity resolves into a member.

bash
$ curl https://members.example.com/wp-json/nwp/v1/entity/me \$   -H "X-NWP-Entity-Token: $ENTITY_TOKEN"
json
{
  "success": true,
  "data": {
    "entity_id": 6,
    "public_id": "nwe_01KXBQKEMBAW4EXM1JD5D7B9YD",
    "platform_id": "5d3c1f4e-9a2b-4c8d-b1e6-2f7a8c9d0e13",
    "status": "anonymous",
    "member_id": null,
    "consent": {
      "granted_at": "2026-07-12 17:57:03",
      "scope": ["engagement", "push"],
      "source": "bingo_join",
      "history": [
        { "event": "granted", "at": "2026-07-12 17:57:03", "scope": ["engagement"], "source": "bingo_join" },
        { "event": "granted", "at": "2026-07-12 17:57:03", "scope": ["push"], "source": "bingo_join" }
      ]
    },
    "created_at": "2026-07-12 17:57:03",
    "last_seen_at": "2026-07-12 17:57:03",
    "activity": [
      {
        "event_type": "consent_granted",
        "payload": { "source": "bingo_join", "entity_id": 6, "member_id": null },
        "created_at": "2026-07-12 17:57:03"
      }
    ],
    "policy": {
      "engagement": { "version": 1, "copy": "I agree to have my event engagement (check-ins, games, votes) tracked..." },
      "push": { "version": 1, "copy": "I agree to receive push notifications from this organization on this device..." }
    },
    "gpc": false
  }
}

GET /entity/me/export

The entity's complete personal-data bundle — descriptor, full consent ledger history, all activity, and push subscription endpoints. Never includes the token hash.

bash
$ curl https://members.example.com/wp-json/nwp/v1/entity/me/export \$   -H "X-NWP-Entity-Token: $ENTITY_TOKEN"
json
{
  "success": true,
  "data": {
    "entity": {
      "public_id": "nwe_01KXBQKEMBAW4EXM1JD5D7B9YD",
      "platform_id": "5d3c1f4e-9a2b-4c8d-b1e6-2f7a8c9d0e13",
      "status": "anonymous",
      "member_id": null,
      "created_at": "2026-07-12 17:57:03",
      "last_seen_at": "2026-07-12 17:57:15",
      "resolved_at": ""
    },
    "consents": [
      { "scope": "engagement", "action": "granted", "source": "bingo_join", "policy_version": "1", "occurred_at": "2026-07-12 17:57:03" },
      { "scope": "push", "action": "granted", "source": "bingo_join", "policy_version": "1", "occurred_at": "2026-07-12 17:57:03" }
    ],
    "contacts": [],
    "activity": [
      {
        "event_type": "consent_granted",
        "payload": { "source": "bingo_join", "entity_id": 6, "member_id": null },
        "created_at": "2026-07-12 17:57:03"
      }
    ],
    "push_subscriptions": []
  }
}

POST /entity/me/erase

Right to erasure, self-service. Revokes all active scopes, deletes push subscriptions, anonymizes activity payloads (event types and timestamps survive, PII does not), and leaves the entity as a revoked, PII-free tombstone. Idempotent — repeating the call still returns success. Consent ledger rows survive as proof of lawful processing.

bash
$ curl -X POST https://members.example.com/wp-json/nwp/v1/entity/me/erase \$   -H "X-NWP-Entity-Token: $ENTITY_TOKEN"
json
{
  "success": true,
  "data": { "erased": true, "entity_id": 6 }
}

GET /platform

The public platform identity descriptor. platform_id is a UUIDv4 minted once per install and never rotated — pair it with an entity's public_id for a globally unique coordinate.

bash
$ curl https://members.example.com/wp-json/nwp/v1/platform
json
{
  "success": true,
  "data": {
    "platform_id": "5d3c1f4e-9a2b-4c8d-b1e6-2f7a8c9d0e13",
    "plugin_version": "1.22.0",
    "identity": { "public_id_format": "nwe_ulid" }
  }
}

Hooks for extension developers

Extensions never touch the identity internals — they fire and listen to WordPress hooks.

nwp_track_entity (action you fire)

Record an engagement event for a consented visitor. Pass the raw token — the platform verifies it, enforces consent (revoked entities accrue nothing new), and writes the ledger row. Fire-and-forget: invalid or missing tokens are silently ignored, so a declined visitor costs you no branching.

php
do_action( 'nwp_track_entity', $token, 'bingo_win', array( 'game_id' => $game_id ) );

Consent lifecycle (actions you listen to)

HookParametersFires when
nwp_entity_consent_granted$entity_id, $consent, $scope, $row_idAn entity-level consent is granted (once per scope, per ledger row).
nwp_entity_consent_revoked$entity_id, $consent, $scope, $row_idAn entity-level consent is revoked.
nwp_member_consent_granted$member_id, $scope, $row_idA member-level consent is granted — the seam consent-gated integrations (e.g. CRM sync) subscribe to.
nwp_member_consent_revoked$member_id, $scope, $row_idA member-level consent is revoked.
php
add_action( 'nwp_member_consent_granted', function ( $member_id, $scope, $row_id ) {
    if ( 'crm_sync' === $scope ) {
        // This member just opted in to CRM sync — start syncing.
    }
}, 10, 3 );

nwp_consent_scopes (filter)

Extend the recognized scope vocabulary. Added scopes are accepted at mint time, versioned by the policy service, and gate integrations like any built-in — write their consent copy on the Consent Policy settings tab.

php
add_filter( 'nwp_consent_scopes', function ( $scopes ) {
    $scopes[] = 'raffle_entries';
    return $scopes;
} );

Contact verification, magic links, and entity merge are coming in a later release — until then contacts is always empty in exports.

Where to go next

For the operator-facing view of the same system — what the ledger means for your audience data, policy versioning, and GDPR workflows — read Consent & your audience data.