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
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:
X-NWP-Entity-Token: 9f1c2ab34d5e6f7089a1b2c3d4e5f601.7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f70819aabbccdA 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):
| Scope | What it permits |
|---|---|
engagement | Tracking event engagement (check-ins, games, votes). Governs whether new activity accrues. |
push | Push notifications to this device. |
email_marketing | Marketing emails. |
sms_marketing | Marketing text messages. |
crm_sync | Syncing 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.
$ 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:
{
"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
| Status | Code | When |
|---|---|---|
| 400 | entity_invalid_scope | Any scope key is unrecognized. data.allowed_scopes lists the valid ones. Rejected before the throttle, so bad payloads never consume quota. |
| 429 | entity_mint_throttled | More than 10 mints per 300 seconds per IP. data.retry_after carries the window in seconds. |
{
"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.
$ curl -X POST https://members.example.com/wp-json/nwp/v1/entity/consent/revoke \$ -H "X-NWP-Entity-Token: $ENTITY_TOKEN"{
"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.
$ curl https://members.example.com/wp-json/nwp/v1/entity/me \$ -H "X-NWP-Entity-Token: $ENTITY_TOKEN"{
"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.
$ curl https://members.example.com/wp-json/nwp/v1/entity/me/export \$ -H "X-NWP-Entity-Token: $ENTITY_TOKEN"{
"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.
$ curl -X POST https://members.example.com/wp-json/nwp/v1/entity/me/erase \$ -H "X-NWP-Entity-Token: $ENTITY_TOKEN"{
"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.
$ curl https://members.example.com/wp-json/nwp/v1/platform{
"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.
do_action( 'nwp_track_entity', $token, 'bingo_win', array( 'game_id' => $game_id ) );Consent lifecycle (actions you listen to)
| Hook | Parameters | Fires when |
|---|---|---|
nwp_entity_consent_granted | $entity_id, $consent, $scope, $row_id | An entity-level consent is granted (once per scope, per ledger row). |
nwp_entity_consent_revoked | $entity_id, $consent, $scope, $row_id | An entity-level consent is revoked. |
nwp_member_consent_granted | $member_id, $scope, $row_id | A member-level consent is granted — the seam consent-gated integrations (e.g. CRM sync) subscribe to. |
nwp_member_consent_revoked | $member_id, $scope, $row_id | A member-level consent is revoked. |
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.
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