Skip to content

Endpoint Schema

Edition 3.1.1

Consent dependency. Every send is gated by effective permissions (allowed/denied). The literals granted/denied/unknown/not_applicable belong to observed signals only; effective conditions always use exactly allowed or denied. See Consent Integration. Four acts are distinct and separately based: transient request parsing, client persistence, endpoint transmission, downstream forwarding. The data plane admits no pending payload.


  1. One common structure for all three events. Every payload (session, conversion, identify) carries the same top-level keys, and the event field says which type it is; only event_data changes shape. The event enum is closed (session | conversion | identify); endpoints reject any other value. Consent changes, revocation, and data-subject requests use separate control routes and never create a normal session, conversion, or identify event.

  2. Transport-agnostic. The payload works with server-side GTM, a custom API, Snowplow, or any system that accepts JSON over HTTP. Platform mapping (GA4 Measurement Protocol, Meta CAPI, Google Ads conversion import) is the endpoint’s responsibility, never the UIAF layer’s. Transport-agnostic is not destination-neutral; see Semantic coupling to destination windows below.

  3. Self-describing. Every payload includes a _meta object declaring the UIAF version, the derived data-quality level, attribution completeness, and the logical emitter. A downstream consumer can inspect _meta first, without parsing the rest, to route or filter.

  4. Consent state always present. The consent object is included in every payload; downstream routing needs it.

  5. Null means absent, within the discriminated null-path rules. A field with no value is null, not omitted, distinguishing “checked, nothing there” from “not checked.” Whole-group nulls are permitted only where the schema declares them (for example privacy-minimized identify sets attribution, page, and client to null; the compact size profile nulls client and, in step two, last_touch.term/content/custom).

  6. Trusted-first initialization. The shared reference validator and the producer runtime are initialized before any untrusted page code executes (the same-realm hardening is sound only under that ordering); normative as UIAF-04-INIT-001 below and mirrored in Implementation Guide.

The immutable body is the UTF-8 RFC 8785 (JCS) serialization, selected and frozen once; its byte length is at most 32768. Retries resend those exact bytes.

The envelope is transport-agnostic, but its content model is optimized for the common paid-media destinations, not neutral.


EventWhenBasis (independent per event)
sessionAutomatic, when the dirty projection changes or an unconsumed current-document create/recovery trigger is pendingEffective analytics_storage == allowed with a resolved (or T0 not_applicable) lifecycle and a documented first-party analytics purpose. Ad permissions alone never authorize a session.
conversionExplicit — application code at a conversion pointAn explicit application action plus that action’s declared purpose and destination.
identifyExplicit — on login/signup, when verified evidence existsVerified identity evidence plus a first-party basis; carries privacy-minimized hashed evidence only — email/phone may use an allowed sha256 profile, while customer is HMAC/server-keyed only.

UIAF does not send on every page load. The system runs on every page (resolving identity, checking attribution), but it is not an analytics pipeline: an automatic session transmits only when the dirty projection changes or an unconsumed current-document create/recovery one-shot trigger is pending. Such a trigger authorizes only an automatic session, never a conversion or identify, and never survives navigation.

Dirty projection (the automatic-send gate)

Section titled “Dirty projection (the automatic-send gate)”

An automatic session should only fire when something actually changed. After resolving identity and attribution, the system builds a canonical RFC 8785 (JCS) projection string over an exact closed tree and compares it byte-for-byte against uiaf_dirty_baseline (sessionStorage, at most 8192 bytes; the stored value is the string, not a hash). Same bytes, no send.

The projection tree is exactly:

{
"attribution": { "count": <int>, "last_touch": <touchpoint|null> },
"consent": {
"effective": <4-permission vector>, "gpc": { "applicable": <bool>, "detected": <bool> },
"signals": <4-signal vector>, "source": <string>, "state_updated_at": <int>, "status": <string>
},
"identity": { "session_id": <uuid|null>, "uid": <uid|null> }
}

consent_record_id is always excluded (receipt rotation alone never triggers a session); is_new, session_start, session_seq, resolution_method, and event_data.reasons are never compared. conversion and identify are explicit and bypass this gate.


{
"event": "session",
"event_id": "fcb7b4d0-abea-4257-a099-f6f723cc2a36",
"timestamp": "2026-07-19T14:30:00.000Z",
"identity": {
"uid": "f81d4fae-7dec-4ec9-a765-00a0c91e6bf6.1647291600",
"session_id": "cf1c74a3-eb75-4dc0-9c44-92adf9b09c29",
"session_seq": 0,
"session_start": true,
"is_new": false,
"resolution_method": "cookie",
"confidence": "high"
},
"consent": {
"signals": {
"analytics_storage": "granted",
"ad_storage": "granted",
"ad_user_data": "granted",
"ad_personalization": "granted"
},
"status": "resolved",
"effective": {
"analytics_storage": "allowed",
"ad_storage": "allowed",
"ad_user_data": "allowed",
"ad_personalization": "allowed"
},
"gpc": { "detected": false, "applicable": false },
"source": "cmp_cookiebot",
"state_updated_at": 1750000000,
"tier": 1
},
"attribution": {
"first_touch": {
"touch_id": "4a26bd7e-0a13-4487-967d-6696e698da69",
"source": "google",
"medium": "cpc",
"campaign": "spring_sale",
"term": null,
"content": null,
"click_ids": {
"gclid": { "value": "Cj0KCQjw84anAbCd", "captured_at": 1750000000, "expires_at": 1790000000 }
},
"referrer": "google.com",
"landing_url": "example.com/products/shoes",
"timestamp": 1750000000,
"custom": {}
},
"last_touch": {
"touch_id": "4a26bd7e-0a13-4487-967d-6696e698da69",
"source": "google",
"medium": "cpc",
"campaign": "spring_sale",
"term": null,
"content": null,
"click_ids": {
"gclid": { "value": "Cj0KCQjw84anAbCd", "captured_at": 1750000000, "expires_at": 1790000000 }
},
"referrer": "google.com",
"landing_url": "example.com/products/shoes",
"timestamp": 1750000000,
"custom": {}
},
"count": 1,
"is_new_touch": false
},
"page": {
"url": "https://example.com/products/shoes",
"path": "/products/shoes",
"referrer": "https://google.com/",
"title": "Running Shoes"
},
"client": {
"user_agent": "Mozilla/5.0",
"language": "en-US",
"viewport": "1920x1080",
"screen": "1920x1080"
},
"event_data": { "reasons": ["session_open"] },
"_meta": {
"uiaf_version": "3.1.1",
"data_quality": "full",
"attribution_completeness": "full",
"emitter": "client"
}
}

Every payload carries the same ten top-level keys, and no others may be added.


FieldTypeDescription
eventenumsession | conversion | identify (closed).
event_idstringUUID v4 ^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$, minted once per logical event by its delivery owner and immutable across retries and transports. The same event_id with a different canonical body is a protocol violation.
timestampstringISO 8601 UTC, validated millisecond precision and Z offset; real-calendar validity is enforced. Every other timestamp in the payload is Unix epoch seconds (integer).
event_dataobjectPer-event discriminated payload — see event_data.
FieldTypeDescription
uidstring | nullCanonical {uuid_v4}.{unix_seconds}, at most 64 bytes, server-minted only (no client-generated persistent-UID fallback). null where persistent identity is prohibited.
session_iduuid v4 | nullEphemeral per-tab session identifier.
session_seqint | nullInteger 0..4294967295 iff session_id is non-null; null when session_id is null. Allocated from uiaf_session_state.next_seq, incremented synchronously before body freeze; strictly increasing, gaps allowed, never reused.
session_startbooleantrue exactly when session_seq === 0.
is_newbooleanAlways false when uid is null.
resolution_methodenumcookie | localstorage_recovery | sessionstorage_recovery | new | ephemeral. Derivations: cookie — the authoritative server-set cookie, including a fresh issuance confirmed by the mandatory post-response re-read; localstorage_recovery / sessionstorage_recovery — a credential-verified recovery whose credential was held in that storage class; ephemeral — no persistent identity resolved. new is a first-issuance report value that the client paths in this specification do not mint (a confirmed first issuance reports cookie); receivers accept it, and under the coherence invariants below it never accompanies is_new: true.
confidenceenumhigh | medium | low — derived, never asserted freely: high — an authoritative cookie read, a fresh server issuance, or a credential-verified recovery after the mandatory post-response cookie crosscheck; medium — a credential-verified recovery adopted in this document before the authoritative crosscheck completes; low — ephemeral or indeterminate resolution.

Identity coherence (normative as UIAF-04-ID-001 below). The identity fields cross-constrain: a resolution_method of cookie, localstorage_recovery, or sessionstorage_recovery requires a non-null uid; ephemeral requires uid: null and confidence: "low"; is_new: true requires a non-null uid and resolution_method: "cookie"; uid: null requires is_new: false. The shared reference validator rejects every other combination.

The consent object has separate stanzas; effective is the only 16-row truth-table domain.

FieldTypeDescription
signalsobjectFour keys (analytics_storage, ad_storage, ad_user_data, ad_personalization), each granted | denied | unknown | not_applicable. Observed-signal values only.
statusenumData-plane lifecycle: resolved | not_applicable only. pending belongs to the internal control plane; no pending payload exists on the data plane.
effectiveobjectFour keys, each allowed | denied. Every effective-permission condition uses exactly these literals.
gpcobject{ detected, applicable } booleans. GPC is an independent overlay and never a source; the overlay applies exactly when detected and applicable are both true, forcing the three ad purposes to denied while analytics is untouched.
sourceenumcmp_cookiebot | cmp_onetrust | cmp_didomi | cmp_custom | gcm | site_policy. cmp_custom covers all unlisted adapters. site_policy appears iff status is not_applicable (and then all four observed signals are not_applicable).
state_updated_atintUnix seconds 0..253402300799 — when the selected source’s consent record last materially changed; never event/read/receipt/serialization time.
consent_record_idstring (optional)^[A-Za-z0-9._:-]{1,128}$, an opaque local reference to a separately retained consent receipt, compared byte-exact. Never a raw CMP string, identifier, secret, credential, or DSR authenticator; excluded from the dirty projection. Raw CMP material (Optanon/CookieConsent/TC-string shapes) is prohibited in any payload, retry body, or log.
tierintDerived shorthand 0..4. Tier never drives permissions, sends, purges, routing, or tests.

first_touch and last_touch are each a touchpoint or null; count is 0..4294967295; is_new_touch is boolean. count === 0 iff both touches are null and is_new_touch is false; a positive count requires both touches present.

Touchpoint (required keys touch_id, source, medium, campaign, term, content, click_ids, referrer, landing_url, timestamp, custom):

FieldTypeDescription
touch_iduuid v4Minted once per touch boundary, before dedup. The boundary is a detected external navigation where one is reliably observable; where a reliable navigation boundary is unavailable, an accepted fingerprint fallback (referrer + landing + click context) determines the boundary instead. Same touch_id = reload/SPA/enrichment continuation; a different touch_id is an accepted new touch even with an identical fingerprint.
source, mediumstring | nullNormalized (trim → NFC → lowercase → NFC); ≤128 UTF-8 bytes; a normalization fixed point.
campaign, term, contentstring | nullNormalized case-preserved (trim → NFC); ≤128 UTF-8 bytes.
click_idsobjectRegistered canonical spellings only (case-sensitive, exact match, no folding): gclid, gbraid, wbraid, dclid, fbclid, msclkid, ttclid, li_fat_id, epik, twclid, ScCid — exact spellings, no aliases or key case-folding; any case variant is invalid. Each value { value (1–512 bytes), captured_at, expires_at } with 0 ≤ captured_at ≤ event epoch < expires_at. expires_at is a unix-second integer capped at the schema ceiling 253402300799 (end of year 9999); within that bound the controller applies a finite configured client-retention ceiling (an unbounded/“never expires” value is not representable). A click occurrence whose expires_at already satisfies now >= expires_at at freeze time is omitted from the frozen body pre-freeze (it never ships), distinct from the post-freeze retry rule that terminally drops a whole queued entry carrying a now-expired click. Canonical click_ids object ≤ 4096 bytes.
referrerstring | nullRegistrable-domain output of the pinned WHATWG-host → IDNA-ASCII → PSL pipeline (not a URL).
landing_urlstring | nullCanonical host (+ non-default port) + serialized path, ≤512 UTF-8 bytes; no scheme, userinfo, query, or fragment.
customobject | nullController-allowlisted attribution parameters, key ^[a-z0-9_]{1,32}$, ≤10 keys, ≤256 bytes/value, ≤ 2048 bytes canonical; reserved UTM/click-ID/uiaf_*/_uiaf_* names rejected. null permitted only on last_touch in the compact size profile.

The whole touchpoint canonical form is at most 6144 UTF-8 bytes.

url = origin + serialized path only (query string and fragment removed); referrer = origin + path only, or null; path matches ^/(?!/)[^?#\\]*$; title ≤512 bytes. Query/fragment/click-ID decoration and raw identifiers are prohibited on every page field.

user_agent (≤512), language, viewport, screen (^[0-9]{1,5}x[0-9]{1,5}$ or null). The whole client group is nullable under the compact size profile; the IP address is never carried in the payload (the endpoint reads it from the request).


{ "reasons": ["session_open"], "extensions": {} }

reasons is a 0–5 unique array from session_open, identity_created, identity_recovered, attribution_touch, consent_change, listed in that registry order when several apply. Each value has an exact derivation, and the shared reference validator rejects an incoherent set (normative as UIAF-04-REASON-001 below):

  • session_open iff identity.session_start is true;
  • identity_created iff identity.is_new is true;
  • identity_recovered iff identity.resolution_method is localstorage_recovery or sessionstorage_recovery;
  • attribution_touch iff attribution.is_new_touch is true;
  • consent_change iff the consent component of the dirty projection differs from the supplied baseline, evaluated only where that comparison is available to the producer.

When none of the five conditions holds, reasons is an empty array. This is the honest encoding for a projection-only identity transition such as divergent-cookie adoption or a non-null UID becoming ephemeral; inventing a reason would violate the closed derivations.

extensions is optional (see below).

{
"event": "conversion",
"event_id": "f1f26ce0-52b3-4baf-9aa1-034cbc72e261",
"timestamp": "2026-07-19T15:45:00.000Z",
"identity": { "uid": "f81d4fae-7dec-4ec9-a765-00a0c91e6bf6.1647291600", "session_id": "cf1c74a3-eb75-4dc0-9c44-92adf9b09c29", "session_seq": 1, "session_start": false, "is_new": false, "resolution_method": "cookie", "confidence": "high" },
"consent": { "signals": { "analytics_storage": "granted", "ad_storage": "granted", "ad_user_data": "granted", "ad_personalization": "granted" }, "status": "resolved", "effective": { "analytics_storage": "allowed", "ad_storage": "allowed", "ad_user_data": "allowed", "ad_personalization": "allowed" }, "gpc": { "detected": false, "applicable": false }, "source": "cmp_cookiebot", "state_updated_at": 1750000000, "tier": 1 },
"attribution": { "first_touch": null, "last_touch": null, "count": 0, "is_new_touch": false },
"page": { "url": "https://example.com/checkout/thank-you", "path": "/checkout/thank-you", "referrer": "https://example.com/checkout/payment", "title": "Order Confirmed" },
"client": { "user_agent": "Mozilla/5.0", "language": "en-US", "viewport": "1920x1080", "screen": "1920x1080" },
"event_data": { "conversion_name": "purchase", "conversion_id": "TXN-2026-04-8834" },
"_meta": { "uiaf_version": "3.1.1", "data_quality": "full", "attribution_completeness": "none", "emitter": "client" }
}

conversion_name matches ^[a-z][a-z0-9_]{0,63}$; conversion_id matches ^[\x21-\x7e]{1,128}$ or is null. A pattern-valid identifier-shaped conversion_id (for example an email-shaped value) is also rejected by the shared prohibited-content screen at producer, endpoint, and validator. extensions is optional (see below), exactly as on the other two discriminators.

Privacy-minimized: the core identify payload sets attribution, page, and client to null (a non-null group needs a registered, versioned deployment profile). event_data.identifiers is a 1–8 array of verified evidence; the canonical array is at most 8192 bytes.

{
"event": "identify",
"event_id": "9c7c2a02-67e9-43c1-8131-5972d3ec66ff",
"timestamp": "2026-07-19T14:32:00.000Z",
"identity": { "uid": "f81d4fae-7dec-4ec9-a765-00a0c91e6bf6.1647291600", "session_id": "cf1c74a3-eb75-4dc0-9c44-92adf9b09c29", "session_seq": 2, "session_start": false, "is_new": false, "resolution_method": "cookie", "confidence": "high" },
"consent": { "signals": { "analytics_storage": "granted", "ad_storage": "granted", "ad_user_data": "granted", "ad_personalization": "granted" }, "status": "resolved", "effective": { "analytics_storage": "allowed", "ad_storage": "allowed", "ad_user_data": "allowed", "ad_personalization": "allowed" }, "gpc": { "detected": false, "applicable": false }, "source": "cmp_cookiebot", "state_updated_at": 1750000000, "tier": 1 },
"attribution": null,
"page": null,
"client": null,
"event_data": {
"identifiers": [
{
"type": "email",
"digest": "b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514",
"algorithm": "sha256",
"normalization_profile": "email_basic_v1",
"verification": { "state": "authenticated", "method": "authenticated_session", "time": 1750000000 },
"provenance": "login-form",
"tenant_binding": "tenant-a"
}
]
},
"_meta": { "uiaf_version": "3.1.1", "data_quality": "full", "attribution_completeness": "none", "emitter": "client" }
}

Each evidence object carries type, digest, algorithm, normalization_profile, verification, provenance, and tenant_binding (and key_version only under HMAC):

  • typeemail | phone | customer. email/phone may use a plain sha256 profile computed client-side; customer must use hmac_sha256 with a server-held key (tenant-separated server-produced keyed digests; the browser never holds the key).
  • digest: lowercase 64-hex. The empty-string SHA-256 digest is a validation error, and raw identifiers never enter the payload.
  • algorithmsha256 | hmac_sha256. key_version is required iff hmac_sha256 and prohibited for sha256; it matches ^[a-z][a-z0-9_-]{0,31}$ (1–32 chars).
  • normalization_profile: matches ^[a-z][a-z0-9_]{1,46}_v[0-9]{1,3}$, at most 52 bytes; the version is part of the identifier. The registry is open: a receiver whose snapshot does not know a pattern-valid profile accepts the enclosing structure, marks that evidence entry unsupported (no matching, confidence, or identity-graph use), and never rejects the whole event.
  • verification: the closed object { state, method, time }: stateauthenticated | verified; methodauthenticated_session | verified_link | verified_otp | payment; time is Unix seconds.
  • provenance and tenant_binding: required strings, each 1–128 bytes.

The sole generic extension point is event_data.extensions: at most 16 top-level dotted keys matching ^[a-z][a-z0-9_]{1,31}\.[a-z][a-z0-9_]{1,31}$, the uiaf. namespace reserved and prohibited, and the whole object at most 4096 canonical bytes.


FieldTypeValues
_meta.uiaf_versionconst"3.1.1".
_meta.data_qualityenumfull | stripped | restricted.
_meta.attribution_completenessenumfull | utm_only | request_context_only | none.
_meta.emitterenumclient | server — the sole logical-emitter field.

_meta has exactly these four keys (additionalProperties: false). data_quality is derived from effective capabilities and field omission, never from a tier:

  • full: all four effective permissions allowed.
  • stripped: analytics_storage allowed with at least one ad permission denied (ad-purpose field groups null-valued, keys present).
  • restricted: a narrowly permitted send under a non-analytics grant.

No payload exists for a pending or all-denied dormant state. _meta carries no transport field (fetch/beacon is never in the immutable body) and no field beyond the four above.


The standard session above is the full-capability example. Two further examples follow: one capability-shaped (stripped, advertising signals denied) and one an explicitly named independently authorized action (restricted):

Stripped — analytics without advertising

Section titled “Stripped — analytics without advertising”

Persistent identity retained; UTMs captured; click IDs excluded because advertising storage is denied. click_ids is {} and _meta signals the reduction.

{
"event": "session",
"event_id": "1a25f07b-49e6-4c66-8ca6-7f2e5a36c05c",
"timestamp": "2026-07-19T14:30:00.000Z",
"identity": { "uid": "f81d4fae-7dec-4ec9-a765-00a0c91e6bf6.1647291600", "session_id": "cf1c74a3-eb75-4dc0-9c44-92adf9b09c29", "session_seq": 0, "session_start": true, "is_new": false, "resolution_method": "cookie", "confidence": "high" },
"consent": {
"signals": { "analytics_storage": "granted", "ad_storage": "denied", "ad_user_data": "denied", "ad_personalization": "denied" },
"status": "resolved",
"effective": { "analytics_storage": "allowed", "ad_storage": "denied", "ad_user_data": "denied", "ad_personalization": "denied" },
"gpc": { "detected": true, "applicable": true },
"source": "cmp_cookiebot", "state_updated_at": 1750000000, "tier": 2
},
"attribution": {
"first_touch": { "touch_id": "4a26bd7e-0a13-4487-967d-6696e698da69", "source": "google", "medium": "cpc", "campaign": "spring_sale", "term": null, "content": null, "click_ids": {}, "referrer": "google.com", "landing_url": "example.com/products/shoes", "timestamp": 1750000000, "custom": {} },
"last_touch": { "touch_id": "4a26bd7e-0a13-4487-967d-6696e698da69", "source": "google", "medium": "cpc", "campaign": "spring_sale", "term": null, "content": null, "click_ids": {}, "referrer": "google.com", "landing_url": "example.com/products/shoes", "timestamp": 1750000000, "custom": {} },
"count": 1, "is_new_touch": false
},
"page": { "url": "https://example.com/products/shoes", "path": "/products/shoes", "referrer": "https://google.com/", "title": "Running Shoes" },
"client": { "user_agent": "Mozilla/5.0", "language": "en-US", "viewport": "1920x1080", "screen": "1920x1080" },
"event_data": { "reasons": ["session_open"] },
"_meta": { "uiaf_version": "3.1.1", "data_quality": "stripped", "attribution_completeness": "utm_only", "emitter": "client" }
}

Restricted — an independently authorized anonymous conversion

Section titled “Restricted — an independently authorized anonymous conversion”

This example is not a generic consent tier. It is one explicitly named, independently authorized action: a payment-fraud conversion sent under a lawful basis that the deployment has separately selected and documented (UIAF does not select or assert that basis), independent of, and not derived from, analytics consent.

{
"event": "conversion",
"event_id": "aeaf13f0-d05a-415c-8aa0-c5a058a529ba",
"timestamp": "2026-07-19T14:30:00.000Z",
"identity": { "uid": null, "session_id": null, "session_seq": null, "session_start": false, "is_new": false, "resolution_method": "ephemeral", "confidence": "low" },
"consent": {
"signals": { "analytics_storage": "denied", "ad_storage": "granted", "ad_user_data": "granted", "ad_personalization": "granted" },
"status": "resolved",
"effective": { "analytics_storage": "denied", "ad_storage": "allowed", "ad_user_data": "allowed", "ad_personalization": "allowed" },
"gpc": { "detected": false, "applicable": false },
"source": "cmp_cookiebot", "state_updated_at": 1750000000, "tier": 3
},
"attribution": { "first_touch": null, "last_touch": null, "count": 0, "is_new_touch": false },
"page": { "url": "https://example.com/checkout/thank-you", "path": "/checkout/thank-you", "referrer": null, "title": "Order Confirmed" },
"client": { "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", "language": "en-US", "viewport": "1280x720", "screen": "2560x1440" },
"event_data": { "conversion_name": "payment_fraud_signal", "conversion_id": null },
"_meta": { "uiaf_version": "3.1.1", "data_quality": "restricted", "attribution_completeness": "none", "emitter": "client" }
}

Strict size limits apply to every payload. The first hop rejects an over-length request with 413 before it even reads the body, and both producer and endpoint enforce per-surface ceilings on the final bytes.

SurfaceMax UTF-8 bytes
Immutable wire/storage body32768
identify identifiers array8192
event_data.extensions object4096
Whole touchpoint6144
click_ids object4096
custom object2048
Per custom value256
Click-ID value1–512
Dirty projection / uiaf_dirty_baseline8192
Pending-context object1024
Pending-context member128

The immutable body is at most 32768 bytes. Selection is a pre-freeze producer decision over a fully assembled standard body, never a runtime fallback and never inferred from an already-compact shape:

  1. If the standard body is ≤ 32768 bytes, send it byte-identical.
  2. Otherwise apply step 1 (client → null); if it now fits, that is the compact body.
  3. Otherwise apply step 2 (last_touch.term, last_touch.content, last_touch.custom → null); if it now fits, that is the compact body.
  4. Still over → emission fails as honest loss.

Because the schema-maximal legal body is below 32768, compact production is currently unreachable with legal members; the compact shape remains a valid receiver wire shape.


These are runtime inter-context coordination primitives, not storage rows: no storage semantics attach and none of these names appears in the storage-key registry. All five names are distinct, and this table mirrors the closed runtime_ipc section of uiaf-enums.json (its sole source).

NamePrimitiveRole
uiaf-session-alloc:<session_id>Web LockLifetime exclusive per-session sequence allocator lock.
uiaf-session-claimBroadcastChannelSession advisory channel.
uiaf-identity-issuanceWeb LockShort-lived origin-wide lock held only around the authoritative re-read + sole-winner cookie-endpoint call + post-response cookie readback.
uiaf-consent-wakeupBroadcastChannelWake-only; messages carry no UID, consent vector, reason, provenance, or policy state. Receipt triggers an authoritative CMP/GPC/server consent-state re-read before any persistence or send decision.
uiaf-retry-drainWeb LockRetry-queue drain coordination (runtime_ipc.retry_drain_web_lock); where Web Locks is unavailable the drainer falls back to a best-effort lease.

These are two distinct contracts and must not be conflated. The session allocator orders events inside one tab’s session; the issuance lock coordinates which tab may call the cookie endpoint.

The 250 ms AbortSignal and pagehide/pageshow.persisted handling are normative here and mirrored in Implementation Guide.


This section defers to uiaf-storage-keys.json; it never hand-maintains a competing list.

Retained (browser storage):

KeyMechanismContents
uiaf_uidcookie (server-set) + LS/SS mirrorPersistent {uuid_v4}.{unix_seconds}.
uiaf_recoveryLS/SS onlyOpaque server-bound recovery credential (never a cookie, never DSR auth).
uiaf_session_statesessionStorage{ session_id, next_seq } — the sole v3 session representation.
uiaf_attributionLS/SSAttribution record (first/last touch, click-ID metadata, count).
uiaf_retry_queuelocalStorageRetry entries keyed by immutable event_id.
uiaf_dirty_baselinesessionStorageThe canonical dirty-projection string (JCS, ≤8192 bytes).

Legacy purge-only (never read, migrated, or written as live v3 state; cleared on sight): uiaf_last_payload_hash, uiaf_pre_click_ids_enc, uiaf_pre_landing_url, uiaf_pre_referrer, uiaf_pre_timestamp, uiaf_pre_utms, uiaf_session, uiaf_session_id. The retired pre-3.0 uiaf_attribution cookie is expired on sight (its purge lives in the live uiaf_attribution row’s metadata, not this list).

Absent by design: attribution backup cookie, uiaf_consent_epoch, uiaf_optout, and _uiaf_token as storage (it is a transient URL parameter only).

One predicate governs the uiaf_uid/uiaf_recovery pair everywhere: lifecycle resolved (or not_applicable under an established T0 policy) and effective analytics_storage allowed and a documented first-party analytics / persistent-identity purpose. The pair is purged atomically on analytics-permission loss, revocation, erasure, and dormant transition. ad_user_data/ad_personalization are routing/disclosure gates, not storage-retention gates.


The machine-readable artifacts (uiaf-payload.schema.json, uiaf-events.json, uiaf-enums.json, uiaf-storage-keys.json) and the append-only release-ledger.json are the versioned surface this document renders. This section is descriptive; nothing here publishes or releases anything. Drafts are never ledgered: the ledger records exactly one entry per released edition, and only the release transaction below appends one.

Schema identity. A released edition’s $id is the exact immutable edition URL https://uiaf.dtv.dev/schema/<edition>/uiaf-payload.schema.json; during pre-release work the $id carries a -draft suffix (https://uiaf.dtv.dev/schema/<edition>-draft/uiaf-payload.schema.json), which the release gate replaces atomically at publication. A release publishes byte-identical edition snapshots under docs-site/public/schema/<edition>/. The /schema/v<major>/ path is only the latest-compatible channel alias and is never used as an $id.

Release ledger shape. One append-only manifest { ledger_version: 1, releases: [...] }. Each release entry is exactly { edition, date, channel, supersedes, artifacts }:

  • edition: a unique released SemVer (drafts are never appended).
  • date: YYYY-MM-DD.
  • channel: matches ^v(0|[1-9][0-9]*)$, its numeral equal to the SemVer major.
  • supersedes: the immediately prior released edition, or JSON null for the first.
  • artifacts: exactly eight unique { path, sha256 } records in lexicographic path order (the four artifact files under both schema/<edition>/ and schema/<channel>/), paths relative to docs-site/public, sha256 lowercase 64-hex, the ledger file itself excluded.

Entries appear in release order.

Atomic publication order. Publication is a single release transaction (snapshot, byte-identical alias, ledger append, protected ref) with no served intermediate state.


These interfaces are control plane: they mint, recover, hand off, revoke, or erase identity state. None of them produces a data-plane event, and none reuses the data-plane envelope. All prose here is explanatory; the normative keywords for this section live in the UIAF-04-AUX-* requirement blocks below.

Cookie endpoint — POST /api/uiaf/cookie (create / recover)

Section titled “Cookie endpoint — POST /api/uiaf/cookie (create / recover)”

Canonical UID form (stated once): ^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.[0-9]{9,11}$, at most 64 characters, server-minted only.

Inbound identity. The only trusted inbound UID is the first-party uiaf_uid cookie. A UID placed in the request body is never trusted: it is rejected with 400 and no cookie is set (normative as UIAF-04-AUX-003). A cookie that fails the canonical form (malformed, wrong shape, or truncated) is treated exactly as expired/absent: the request is handled as a fresh create.

Two operations. create asks the server to mint a new UID. recover presents only the opaque uiaf_recovery credential (never a UID, never a raw identifier) and, on success, re-binds the prior UID. Recovery is the sole path by which a pre-existing UID is returned.

Origin authority. The endpoint is Origin-authoritative, with exact precedence: a valid Origin with absent Sec-Fetch-Site is accepted; a Sec-Fetch-Site value of cross-site rejects; and a missing or invalid Origin can never establish same-origin (SFS alone cannot stand in for it). Sec-Fetch-Site only corroborates a valid Origin. Requests are strict application/json only.

Candidate-only adoption. The returned UID is candidate-only until a mandatory post-response authoritative cookie re-read. Client code never rewrites a healthy matching cookie and never overwrites a divergent valid cookie. It adopts the authoritative cookie value and updates only its own script-storage mirror (no-overwrite).

Hardening. The endpoint is served only over TLS, returns with Cache-Control: no-store, rate-limits per source, and logs zero raw UID material.

_uiaf_token is an opaque, single-use, exactly-bound token (≥128 CSPRNG bits, base64url, 22–43 characters), a URL parameter only and never storage. At mint it is bound to the tenant, the issuer, the exact target audience, the purpose, and the exact clean target URL, and is redeemable exactly once.

Mint: POST /api/uiaf/handoff, a top-level same-origin form navigation (application/x-www-form-urlencoded); it is never a script-exposed fetch/XHR.

Redeem: POST /api/uiaf/handoff/redeem, server-to-server, Content-Type: application/json, body exactly { token, target } where target is the exact clean absolute URL being handled (the destination URL with _uiaf_token removed). Success is 200 { uid }.

Navigation discipline. The target server redeems the token before returning HTML or any subresource and issues its own clean 303 before render. A redemption failure (any 4xx/410/5xx above) never unconditionally mints a UID. The target proceeds as an ordinary first-party visit and mints only if its own base predicate independently authorizes it. There is no static-page, prefetch, <script>-injection, or history.replaceState fallback path. The handoff never overwrites an existing valid destination cookie; where a valid cookie is already present it may record an idempotent alias/link only (adopt-then-verify, as with the cookie endpoint). The token value is suppressed from all server and access logs (normative as UIAF-04-AUX-005 below).

Revocation and Data-Subject-Request handling are control-plane operations that emit no data-plane event and never reuse the payload envelope.

Revocation is a same-origin, CSRF-protected, idempotent, non-enumerating browser request that is control-plane tombstone-only: it records a server-side revocation tombstone and does not write any persistent browser marker (there is no uiaf_optout). It may reference the prior UID solely to honour the withdrawal before purge. The local purge and the dormant (all-denied) transition proceed on their own predicates independently of the call’s outcome. A network failure never blocks the browser-side purge. Revocation is not queued for retry and is not, by itself, an erasure (normative as UIAF-04-AUX-006 below).

DSR (erasure, access, portability) is a privileged, server-side operation gated on authenticated verification of the requester through the deployment’s own account/identity channel, executed as a downstream cascade across the deployment’s stores with asynchronous status reporting. A uiaf_uid, any derived hash, or the uiaf_recovery credential is never an authenticator for a DSR. None proves account ownership, and none may stand in for authenticated identity (normative as UIAF-04-AUX-007 below).


Section titled “Pending-context allowlist (pre-consent buffer)”

Before consent resolves there is no payload and no send. The only thing the runtime may retain across the pre-resolution moment is a tiny, strictly bounded pending-context buffer, and its shape is a closed allowlist, not free-form capture.

Section titled “Consent validation at the receiving endpoint”

The consent object is validated by the receiver, and several rejections are universal: applied to every event regardless of discriminator:

  • A data-plane payload whose status is pending (the data plane admits no pending payload).
  • A payload where GPC is detected and applicable yet any of the three ad-purpose effective values is allowed (a self-contradictory vector: GPC forces all three ad purposes to denied).
  • A tier shorthand that is inconsistent with its derivation. Tier derives only from the effective vector, with a status of not_applicable overriding to tier 0; observed signals are not part of tier derivation. Tier is derived shorthand and never drives behavior; a mismatch is rejected, never reconciled.
  • Any structural, closed-enum, or bounds violation in the object.

The signals → effective consistency check runs against the receiver’s own configured policy, not against anything learned from the payload.


async function sendViaFetch(endpoint_url, frozen_body) {
try {
// fetch() returns a Promise; a network failure REJECTS it (no status is produced).
response = await fetch(endpoint_url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: frozen_body // the immutable RFC 8785 bytes — never re-serialized or mutated
})
} catch (networkError) {
queueForRetry(frozen_body) // network rejection is retryable; the frozen body is untouched
return
}
if response.status >= 200 AND response.status < 300 { return } // any 2xx is success
else if response.status >= 500 AND response.status < 600 { queueForRetry(frozen_body) } // 5xx transient: retry the exact bytes
else if response.status == 429 { queueForRetry(frozen_body, validatedRetryAfter(response)) } // rate-limited: same bytes + event_id; bounded (<=3600 s) validated Retry-After hint only
else { observe(response.status) } // 3xx (unexpected) + other 4xx: terminal config/protocol failure — drop + observe, never retry
}

Used during unload (pagehide, or visibilitychange with visibilityState === "hidden").

sendBeacon(...) === true means the user agent accepted the body into its send queue. It is not an endpoint acknowledgement, and a queued body may still be dropped by the agent.

Durable-retry contract on the unload path. When the event’s purpose permits durable retry storage and that storage is available, the producer durably enqueues the immutable body (byte-identical, same event_id) before or alongside sendBeacon, and retains that entry until a later endpoint-acknowledged (2xx) drain removes it. A sendBeacon returning true is user-agent queue acceptance only and never removes the durable entry; a false return leaves the entry queued. When durable storage is forbidden, unavailable, or quota-failed, the beacon is honest best-effort and loss is possible. In no case does sendBeacon returning true, on its own, advance uiaf_dirty_baseline or consume a current-document create/recovery trigger; those occur only on a 2xx acknowledgement or the permitted durable enqueue.

The browser is the default owner of the automatic session. The application server may own and POST that automatic session’s frozen body directly only when it is authoritative over all four inputs: the prior dirty projection, the current consent/policy vector, the session-sequence allocation, and committed delivery.

When a send fails transiently, the frozen body waits in uiaf_retry_queue, a JSON map keyed by immutable event_id, and is retried unchanged.

What is queued and later sent is only the byte-identical frozen body; there is no post-freeze re-serialization. A body that cannot be re-sent unchanged is honestly lost, never rewritten. Bounded exponential backoff with jitter sets next_attempt_at; a validated Retry-After hint from a 429 floors it (bounded to 3,600 seconds; an absent, malformed, or excessive hint is ignored). The 72-hour TTL is derived from the entry’s enqueued_at (there is no separate entry expiry field); there is an attempt cap.

Terminal-drop conditions each remove one whole entry (never a field-level redaction, never a partial resend) and leave every other, still-eligible entry untouched:

  • TTL / capability loss: an entry is dropped when now >= enqueued_at + 72h, and independently when its immutable required_permissions, purpose, or destinations are no longer satisfied by current capability (a downgrade drops only the newly-unsatisfied entries before their next send, never redacted or resent under the same event_id).
  • Stale click bytes: a frozen body carrying any click-ID occurrence whose in-payload expires_at satisfies now >= expires_at at send time is terminally dropped whole.

Unrelated entries whose permissions/purpose/destinations remain satisfied survive a downgrade and continue on their own schedule. The queue is only persisted at all when the event’s own purpose permits storage; when storage is not permitted, or is malformed or quota-failed, delivery degrades to honest loss rather than an unpersisted retry. Queue draining uses the registered Web Lock uiaf-retry-drain (runtime_ipc.retry_drain_web_lock in uiaf-enums.json) where the API is available and a best-effort lease otherwise; without the lease the drainer may issue a duplicate send, which the endpoint absorbs through event_id deduplication.


BCP 14 requirement keywords are normative only inside the identified blocks below; all other prose in this section is explanatory.

  • UIAF-04-EP-001 — The endpoint MUST validate the raw body with the shared reference validator (byte size ≤ 32768, duplicate-member rejection, RFC 8785 canonical-bytes equality, closed enums, and the prohibited-content screens) before processing, and MUST reject anything failing them.
  • UIAF-04-EP-002 — The producer MUST treat a network failure, a 5xx, or a 429 as retryable and every other 4xx as terminal; a 429 retry MUST resend the byte-identical frozen body under the same event_id, honoring a Retry-After hint only when it parses as non-negative delta-seconds or a future HTTP-date and does not exceed the bounded ceiling of 3,600 seconds — an absent, malformed, or excessive hint falls back to the standard bounded backoff. It MUST classify an unexpected final 3xx as a terminal configuration/protocol failure — observe/drop, never retried and never treated as success — and MUST NOT signal success on any rejected body.
  • UIAF-04-EP-003 — The endpoint MUST deduplicate by immutable event_id with a retention window of at least 72 hours, order accepted events by (session_id, session_seq) with a receipt-time tiebreak, and MUST reject a repeated event_id carrying a different canonical body.
  • UIAF-04-EP-004 — Public direct ingest is untrusted: the endpoint MUST enforce bounds, quota, and zero privileged effects rather than assert forgery rejection; a same-origin relay derives or replaces server-authoritative fields (today exactly _meta.emitter, forced to client for browser submissions), and only an authenticated server-to-server channel may assert emitter: "server". An unauthenticated ingress whose body asserts emitter: "server" MUST be hard-rejected with a 4xx — never marked-and-admitted (the normative trust requirement lives in Data Handling). The browser holds no secrets; SameSite, Path, CORS, and first-party naming are never described as authentication or isolation.
  • UIAF-04-AUX-001uiaf_uid MUST be server-minted only (there is no client persistent-UID fallback) and purged atomically with uiaf_recovery on analytics-permission loss, revocation, erasure, and dormant transition. Any server-side refresh is a separate server-owned renewal lifecycle that preserves the same UID value and authority; it MUST NOT be read as authorizing the cookie route (UIAF-04-AUX-003) to overwrite an already-valid cookie, which stays in the route’s safe no-overwrite branch. The base persistence predicate MUST govern both the endpoint’s Set-Cookie on create and every browser write/retention of the atomic uiaf_uid/uiaf_recovery pair. A healthy or divergent valid server cookie is authoritative and MUST NOT be client-overwritten; the client adopts it and updates only its own script-storage mirror.
  • UIAF-04-AUX-002uiaf_recovery MUST be opaque, bounded, LS/SS only, never a cookie, never logged raw, and never a DSR authenticator. It MUST be server-bound to its UID, tenant, credential version, expiry, and continuity policy, and MUST honor expiry, revocation, and rotation (an expired, revoked, or rotated-out credential is refused); it MUST be returned only over TLS with Cache-Control: no-store.
  • UIAF-04-SESS-001uiaf_session_state MUST be { session_id, next_seq }; a corrupt or oversize value MUST mint a new UUID-v4 session with next_seq 0.
  • UIAF-04-ATTR-001uiaf_attribution MUST strip click-ID members on effective ad_storage loss and delete the whole record on analytics-permission loss, revocation, erasure, or dormant transition; storage loss mints, never reconstructs, touch identifiers.
  • UIAF-04-RETRY-001uiaf_retry_queue entries MUST carry immutable required_permissions/purpose/destinations and MUST be dropped (never redacted or resent under the same event_id) when those permissions are no longer satisfied.
  • UIAF-04-DIRTY-001uiaf_dirty_baseline MUST hold the canonical JCS projection string (≤8192 bytes), advance only after a 2xx acknowledgement or durable enqueue, and be purged on every applicable downgrade/revocation/erasure path and on the terminal drop of a queued entry whose durable enqueue advanced it — the purge forces re-evaluation against an absent baseline so still-current state is re-emitted rather than silently treated as delivered.
  • UIAF-04-AUX-003 — The cookie endpoint (POST /api/uiaf/cookie) MUST enforce the full route contract: the only canonical inbound UID is the first-party uiaf_uid cookie. When any already-valid uiaf_uid cookie is present — whether it matches or diverges from server state — the route MUST emit no Set-Cookie and MUST NOT replace or refresh it; that browser-held valid cookie remains authoritative and the client adopts/mirrors it. A cookie failing the canonical form MUST be treated as absent (expired) and MAY follow a fresh create under the base persistence predicate. create carries no inbound UID and mints server-side; any body-supplied UID MUST be rejected with 400 and set no cookie; recover carries only uiaf_recovery and additionally MUST satisfy the base persistence predicate and the deployment’s continuity policy. The endpoint MUST be Origin-authoritative — a valid Origin with absent Sec-Fetch-Site accepted, a Sec-Fetch-Site of cross-site rejected, and a missing or invalid Origin unable to establish same-origin — MUST accept strict application/json only, MUST rate-limit per source, and MUST log zero raw UID on every path. A returned UID MUST be candidate-only until a mandatory post-response authoritative cookie re-read before any persist or send; an unreadable re-read is indeterminate, while an authoritatively established uid: null survives.
  • UIAF-04-INIT-001 — The shared reference validator and producer runtime MUST be initialized before any untrusted page code executes; the same-realm intrinsic-capture hardening is relied upon only under that ordering.
  • UIAF-04-EP-005 — When a navigator.sendBeacon body is sent cross-origin as non-safelisted application/json, the endpoint MUST explicitly handle the CORS preflight — answering the OPTIONS request with the appropriate CORS response headers — and MUST tolerate the beacon Content-Type; alternatively the producer MUST send a CORS-safelisted body that the endpoint resolves through content-negotiation. sendBeacon returning true is a user-agent queue acceptance, never an endpoint acknowledgement.
  • UIAF-04-EP-006 — On the unload path, when the event’s purpose permits durable retry storage and that storage is available, the producer MUST durably enqueue the byte-identical immutable body (same event_id) before or alongside sendBeacon and MUST retain it until a 2xx-acknowledged drain; a sendBeacon true/false return MUST NOT remove or advance that entry, and MUST NOT on its own advance uiaf_dirty_baseline or consume a current-document create/recovery trigger. Where durable storage is forbidden, unavailable, or quota-failed, the beacon MAY be honest best-effort with possible loss.
  • UIAF-04-EP-007 — The first hop (same-origin relay or public ingest) MUST enforce a hard Content-Length ceiling — 40,960 bytes RECOMMENDED — before buffering or parsing the request body, rejecting over-limit requests with 413 without parsing. The 32,768-byte body ceiling and the sub-object caps are post-parse ceilings on the canonical (JCS) form and are no substitute for this guard.
  • UIAF-04-RETRY-002 — A uiaf_retry_queue entry MUST be the closed object { payload, enqueued_at, attempts, next_attempt_at, bytes, required_permissions, purpose, destinations } with no additional members: payload MUST be a JSON string whose UTF-8 encoding is byte-identical to the frozen RFC 8785 body (recovery is exactly UTF-8 encoding, never re-serialization); enqueued_at/next_attempt_at MUST be integer Unix seconds in 0..253402300799; attempts MUST be an integer 0..4294967295; and required_permissions/purpose/destinations MUST reference the consent contract’s permission names and the deployment’s configured purpose/destination vocabularies — never a free-form or competing list. A malformed entry MUST be dropped individually and a whole-map parse failure MUST treat the queue as absent, each with one bounded diagnostic.
  • UIAF-04-TRIG-001 — A current-document create/recovery trigger MUST follow the state machine pending → in_flight → acknowledged | durably_enqueued | terminal | expired: a failed non-queued attempt returns it to pending under a bounded in-document retry schedule (at most 3 further attempts, bounded backoff with jitter); a consent downgrade that de-authorizes the event MUST move it to terminal and cancel scheduled work; navigation or document destruction MUST move an unconsumed trigger to expired with no cross-document replay; and exhausting the schedule without a 2xx or durable enqueue MUST abandon the trigger as honest loss.
  • UIAF-04-REASON-001 — A session payload’s event_data.reasons MUST contain session_open iff identity.session_start is true, identity_created iff identity.is_new is true, identity_recovered iff identity.resolution_method is localstorage_recovery or sessionstorage_recovery, attribution_touch iff attribution.is_new_touch is true, and consent_change iff the consent component of the dirty projection differs from the supplied baseline where that comparison is available. When none of those five conditions holds but a session is authorized by another dirty-projection delta, reasons MUST be the empty array; producers MUST NOT fabricate a reason merely to satisfy a non-empty shape. Multiple values MUST appear in registry order.
  • UIAF-04-ID-001 — The identity group MUST be internally coherent: resolution_method of cookie, localstorage_recovery, or sessionstorage_recovery requires a non-null uid; ephemeral requires uid: null and confidence: "low"; is_new: true requires a non-null uid and resolution_method: "cookie"; uid: null requires is_new: false. confidence MUST be derived — high only for an authoritative cookie read, a fresh issuance, or a post-crosscheck credential-verified recovery; medium only for a credential-verified recovery adopted before the authoritative crosscheck; low for ephemeral or indeterminate resolution.
  • UIAF-04-AUX-008 — The cookie endpoint MUST implement the closed shapes and exhaustive status map of its route contract: request bodies exactly {} (create) or { "uiaf_recovery": "<credential>" } (recover) as strict duplicate-member-rejecting JSON with no additional members, uiaf_recovery the sole discriminator and any body UID 400; 200 responses carry exactly {uid, recovery} — issuance with Set-Cookie and non-null recovery (a rotated credential supersedes its predecessor, which is refused thereafter), no-set with both null and no Set-Cookie; every denial is the non-enumerating 403 with exactly {"error": "denied"}; and the only statuses are 200/400/403/429/5xx. The response uid is candidate-only until the mandatory post-response cookie re-read.
  • UIAF-04-AUX-004 — The handoff mint (POST /api/uiaf/handoff) MUST be a top-level same-origin form navigation (never a script-exposed fetch) whose body is exactly { target, purpose, csrf_token }target an allowlisted exact clean absolute URL of 1–2048 bytes, purpose the constant identity_handoff, csrf_token a base64url value of 22–128 characters that MUST encode at least 128 bits of CSPRNG entropy and MUST be bound to the initiating session (length alone is not sufficient). It MUST mint a single-use _uiaf_token that is base64url of 22–43 characters encoding ≥128 bits of CSPRNG entropy, bound to tenant, issuer, exact target audience, purpose, and exact clean target, with TTL default 120 s (configurable 30–300 s). The minted token MUST NOT be returned to client script or embedded in any static or prefetchable link, and there MUST be no client-render, <script>-injection, static-page, prefetch, or history.replaceState fallback path. Only a successful mint (303 with _uiaf_token) and a valid-target policy denial or internal mint failure (clean 303 without a token) MUST be redirects; a malformed or non-allowlisted target MUST be 400, a same-origin/CSRF failure MUST be 403, and a source rate limit MUST be 429, none of which are redirects.
  • UIAF-04-AUX-005 — The handoff redeem (POST /api/uiaf/handoff/redeem) MUST be server-to-server with Content-Type: application/json and a body of exactly { token, target }, where target is the exact clean absolute URL being handled (the destination URL without the _uiaf_token parameter); this content-type and body shape MUST be enforced before any binding or status logic. It MUST authenticate the caller and enforce single use; it MUST return 200 { uid } only on an exact binding match and otherwise 400/401/403 (any binding mismatch) or a non-enumerating 410 covering unknown, expired, and already-replayed/consumed tokens indistinguishably, with 429/503 as applicable. The target server MUST redeem before returning HTML or any subresource and issue a clean 303 before render, MUST NOT unconditionally mint a UID on redemption failure, MUST NOT overwrite an existing valid destination cookie (idempotent alias/link only), and MUST NOT provide any client-render, <script>-injection, static-page, prefetch, or history.replaceState fallback. On every branch it MUST suppress the raw token and any token-bearing query string from all access and application logs.
  • UIAF-04-AUX-006 — Revocation MUST be a same-origin, CSRF-protected, idempotent, non-enumerating, control-plane, tombstone-only request that writes no persistent browser marker; it MUST NOT emit a data-plane event or reuse the payload envelope. Local purge and the dormant transition MUST proceed independently of the call’s outcome; the prior UID MAY be used solely to honor the withdrawal before local purge and MUST NOT be reused for any other purpose; revocation MUST NOT be queued for retry and is not, by itself, an erasure.
  • UIAF-04-AUX-007 — A privileged DSR (erasure, access, portability) MUST be gated on authenticated verification of the requester through the deployment’s own account/identity channel and executed as a downstream cascade with asynchronous status; it MUST NOT emit a data-plane event or reuse the payload envelope; and a uiaf_uid, any derived hash, or the uiaf_recovery credential MUST NOT authenticate it.

Assembly copies before it derives; it never mutates caller state. Consent constraints are applied at collection time, so by the time the body is assembled the data already reflects them. The discriminator is never defaulted: the specific event is authorized by its own predicate before any session sequence is allocated, any body is frozen, or anything is sent. There is no fallthrough that turns a missing or unknown discriminator into a session.

Each event has an independent authorization predicate. Only the automatic session consults the dirty projection, and it is authorized when the projection has changed or an unconsumed current-document create/recovery one-shot trigger is pending; conversion and identify are explicit and bypass the projection entirely, each carrying its own basis (a trigger never authorizes them). No event is authorized while consent is pending, and an all-denied (dormant) effective vector produces no payload for any discriminator.

// Step 1 — authorize the SPECIFIC event. There is no default discriminator.
function authorizeEvent(context, consent) {
var eff = consent.effective
if consent.status != "resolved" AND consent.status != "not_applicable" { return DENY } // no pending payload
if allDenied(eff) { return DENY } // dormant: no payload at all
switch (context.event) { // must be present and one of the three
case "session": // automatic; projection- or trigger-gated
if eff.analytics_storage != "allowed" { return DENY } // ad permissions alone never authorize a session
if NOT documentedAnalyticsPurpose(context){ return DENY }
// authorized by a changed projection OR an unconsumed current-document create/recovery trigger
if NOT (dirtyProjectionChanged(context)
OR unconsumedCurrentDocTrigger(context)) { return DENY }
return ALLOW
case "conversion": // explicit action; NOT projection-gated
if NOT authorizedConversionAction(context){ return DENY } // its own independently authorized basis
return ALLOW
case "identify": // explicit assertion; NOT projection-gated
if NOT authorizedIdentityAssertion(context){ return DENY }
return ALLOW
default:
return DENY // absent/unknown discriminator is never a session
}
}
function assembleBody(context, consent) {
if authorizeEvent(context, consent) == DENY { return NO_PAYLOAD } // never allocate/freeze/send
var event = context.event // explicit; never defaulted
var eff = consent.effective
var identity = copyIdentity(context.identity) // copy-before-derive
if eff.analytics_storage != "allowed" {
identity.uid = null // no persistent id without analytics
identity.is_new = false
}
// Discriminator-specific groups and null-paths.
var attribution, page, client
if event == "identify" {
attribution = null; page = null; client = null // privacy-minimized identify
} else {
attribution = copyAttribution(context.attribution)
OR { first_touch: null, last_touch: null, count: 0, is_new_touch: false }
if eff.ad_storage != "allowed" {
stripClickIds(attribution.first_touch) // on copies
stripClickIds(attribution.last_touch)
}
page = renderPageStripped(context.page) // origin+path, no query/fragment
client = renderClient() // compact size profile may null this pre-freeze
}
// data_quality is DERIVED from effective capabilities, never from a tier.
var data_quality =
allAllowed(eff) ? "full"
: eff.analytics_storage == "allowed" ? "stripped" // >=1 ad denied
: "restricted" // explicit event under a non-analytics basis
var payload = {
event: event,
event_id: context.event_id, // minted once, immutable across retries
timestamp: nowIso8601Millis(),
identity: identity,
consent: renderConsent(consent), // signals/status/effective/gpc/source/state_updated_at/tier
attribution: attribution,
page: page,
client: client,
event_data: renderEventData(event, context), // reasons | {conversion_name,conversion_id} | {identifiers}
_meta: {
uiaf_version: "3.1.1",
data_quality: data_quality,
attribution_completeness: deriveCompleteness(attribution), // from ACTUAL last_touch content; never invented
emitter: "client"
}
}
return freezeCanonical(payload) // RFC 8785 bytes, selected once via the size profiles
}

deriveCompleteness reports what the frozen body actually carries and never fabricates coverage, mirroring the reference validator’s coherence check. When attribution is null (privacy-minimized identify), completeness is none and unconstrained by touch content. When last_touch is present, derivation follows one precedence order over its actual content: any click_ids member present ⇒ full; else any UTM surface (source, medium, campaign, term, content) non-null ⇒ utm_only; else a request-context surface (referrer or landing_url) non-null ⇒ request_context_only; else none. Declaring a value that this precedence does not derive (none over real content, full over a UTM-only touch, or any other mismatch) is rejected as attribution_completeness_incoherent.

function sendToEndpoint(frozen_body) {
if frozen_body == NO_PAYLOAD { return } // nothing was authorized; never send
drainRetryQueue(config.endpoint_url) // keyed by event_id
if document.visibilityState == "hidden" {
// Unload path. If this event's purpose permits durable retry storage AND it is available,
// durably enqueue the byte-identical body (same event_id) BEFORE/ALONGSIDE the beacon and
// retain it until a later endpoint-acknowledged (2xx) drain.
if durableRetryPermittedAndAvailable(frozen_body) {
queueForRetry(frozen_body) // durable entry; NOT removed by sendBeacon===true
sendViaBeacon(config.endpoint_url, frozen_body) // true = UA-queue acceptance only; false leaves entry queued
} else {
sendViaBeacon(config.endpoint_url, frozen_body) // storage forbidden/unavailable/quota-failed => honest best-effort; loss possible
}
} else {
sendViaFetch(config.endpoint_url, frozen_body)
}
// NB: neither uiaf_dirty_baseline advancement nor current-document create/recovery trigger
// consumption happens because sendBeacon returned true — only on a 2xx ack or the permitted durable enqueue.
}

Data-flow text equivalent (non-normative; mirrors the diagram above for readers without the rendered image). This restates the diagram’s branches and order and adds no requirements:

  1. Page load: the browser requests a page; the server reads HTTP headers (cookie, referrer, UTMs).
  2. Read consent: the app runs readConsent() to resolve the effective vector (section 07).
  3. While pending: only the registered pending-context buffer is held, and nothing is sent, written, or persisted.
  4. If analytics_storage allowed: the app runs resolveIdentity() (reads/creates uid + session_state in cookie/localStorage) and captureAttribution() as post-resolution authorized capture (UTMs when authorized, click IDs only if ad_storage is allowed). Otherwise (analytics denied): no persistent identity (uid null) and no automatic session.
  5. Authorize the specific event: authorizeEvent(context, consent) picks the specific discriminator with no default.
  6. If event == session (analytics allowed + documented purpose + [projection changed OR an unconsumed create/recovery trigger]): build the dirty projection and compare it to uiaf_dirty_baseline (or honor the pending one-shot trigger). If event == conversion/identify (explicit basis): verify the event’s own authorization; the projection gate is bypassed.
  7. Freeze: allocate the session sequence, then assembleBody() and freezeCanonical() (RFC 8785), only if an event was authorized.
  8. Deliver: POST the frozen body (fetch or sendBeacon) to the endpoint.
  9. Endpoint: the shared reference validator runs, events are deduplicated by event_id and ordered by (session_id, session_seq), then routed to downstream platforms by effective consent and _meta.
  10. Baseline: advance uiaf_dirty_baseline only on a 2xx acknowledgement or durable enqueue, and only for a session.

Body would exceed 32768 bytes. There is no runtime oversize fallback: the producer applies the size profiles pre-freeze (step 1 nulls client, step 2 nulls last_touch.term/content/custom); if it still exceeds the cap, emission is honest loss. Because the schema-maximal legal body is well under 32768, a legal body never reaches this branch.

No event on every page load. The system runs on every page but an automatic session transmits only when the dirty projection changes or an unconsumed current-document create/recovery trigger is pending. The endpoint detects new visitors (is_new), recoveries (resolution_method), and new attribution (is_new_touch) from payload fields.

Consent changes mid-session. The next automatic evaluation reflects the new effective vector; already-sent frozen bodies are never retroactively modified. Not every downgrade purges identity: the base-persistence pair (uiaf_uid + uiaf_recovery) is deleted atomically only on loss of the analytics-storage permission / persistence predicate, revocation, erasure, a dormant (all-denied) transition, or a reconcile-on-init that finds a prohibited state. An ad-purpose-only loss (e.g. ad_storage denied while analytics_storage stays allowed) retains the pair and instead applies the narrower strips: click-ID members removed from uiaf_attribution and ad-routing suppressed. Retry entries are dropped when their immutable required_permissions no longer hold, independent of the identity-purge decision.

Clock skew. timestamp is client-generated; the endpoint records its own receipt time for ordering and dedup. The client timestamp is advisory, not a compliance timestamp.

Endpoint unreachable. The frozen body is queued under uiaf_retry_queue (keyed by event_id, 72-hour TTL, attempt cap). If storage is unavailable or quota-failed, the send is honestly lost. UIAF is analytics infrastructure, not a transactional system.