Endpoint Schema
Edition 3.1.1
Consent dependency. Every send is gated by effective permissions (
allowed/denied). The literalsgranted/denied/unknown/not_applicablebelong to observed signals only; effective conditions always use exactlyallowedordenied. See Consent Integration. Four acts are distinct and separately based: transient request parsing, client persistence, endpoint transmission, downstream forwarding. The data plane admits nopendingpayload.
Design Principles
Section titled “Design Principles”-
One common structure for all three events. Every payload (
session,conversion,identify) carries the same top-level keys, and theeventfield says which type it is; onlyevent_datachanges 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 normalsession,conversion, oridentifyevent. -
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.
-
Self-describing. Every payload includes a
_metaobject declaring the UIAF version, the derived data-quality level, attribution completeness, and the logicalemitter. A downstream consumer can inspect_metafirst, without parsing the rest, to route or filter. -
Consent state always present. The
consentobject is included in every payload; downstream routing needs it. -
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-minimizedidentifysetsattribution,page, andclienttonull; the compact size profile nullsclientand, in step two,last_touch.term/content/custom). -
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.
Semantic coupling to destination windows
Section titled “Semantic coupling to destination windows”The envelope is transport-agnostic, but its content model is optimized for the common paid-media destinations, not neutral.
Event Types
Section titled “Event Types”| Event | When | Basis (independent per event) |
|---|---|---|
session | Automatic, when the dirty projection changes or an unconsumed current-document create/recovery trigger is pending | Effective 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. |
conversion | Explicit — application code at a conversion point | An explicit application action plus that action’s declared purpose and destination. |
identify | Explicit — on login/signup, when verified evidence exists | Verified 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.
Full Payload Schema (standard session)
Section titled “Full Payload Schema (standard session)”{ "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.
Field Reference
Section titled “Field Reference”Top-level
Section titled “Top-level”| Field | Type | Description |
|---|---|---|
event | enum | session | conversion | identify (closed). |
event_id | string | UUID 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. |
timestamp | string | ISO 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_data | object | Per-event discriminated payload — see event_data. |
identity
Section titled “identity”| Field | Type | Description |
|---|---|---|
uid | string | null | Canonical {uuid_v4}.{unix_seconds}, at most 64 bytes, server-minted only (no client-generated persistent-UID fallback). null where persistent identity is prohibited. |
session_id | uuid v4 | null | Ephemeral per-tab session identifier. |
session_seq | int | null | Integer 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_start | boolean | true exactly when session_seq === 0. |
is_new | boolean | Always false when uid is null. |
resolution_method | enum | cookie | 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. |
confidence | enum | high | 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.
consent (data-plane)
Section titled “consent (data-plane)”The consent object has separate stanzas; effective is the only 16-row truth-table domain.
| Field | Type | Description |
|---|---|---|
signals | object | Four keys (analytics_storage, ad_storage, ad_user_data, ad_personalization), each granted | denied | unknown | not_applicable. Observed-signal values only. |
status | enum | Data-plane lifecycle: resolved | not_applicable only. pending belongs to the internal control plane; no pending payload exists on the data plane. |
effective | object | Four keys, each allowed | denied. Every effective-permission condition uses exactly these literals. |
gpc | object | { 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. |
source | enum | cmp_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_at | int | Unix seconds 0..253402300799 — when the selected source’s consent record last materially changed; never event/read/receipt/serialization time. |
consent_record_id | string (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. |
tier | int | Derived shorthand 0..4. Tier never drives permissions, sends, purges, routing, or tests. |
attribution
Section titled “attribution”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):
| Field | Type | Description |
|---|---|---|
touch_id | uuid v4 | Minted 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, medium | string | null | Normalized (trim → NFC → lowercase → NFC); ≤128 UTF-8 bytes; a normalization fixed point. |
campaign, term, content | string | null | Normalized case-preserved (trim → NFC); ≤128 UTF-8 bytes. |
click_ids | object | Registered 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. |
referrer | string | null | Registrable-domain output of the pinned WHATWG-host → IDNA-ASCII → PSL pipeline (not a URL). |
landing_url | string | null | Canonical host (+ non-default port) + serialized path, ≤512 UTF-8 bytes; no scheme, userinfo, query, or fragment. |
custom | object | null | Controller-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.
client
Section titled “client”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).
event_data (per-event discriminator)
Section titled “event_data (per-event discriminator)”session
Section titled “session”{ "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_openiffidentity.session_startistrue;identity_creatediffidentity.is_newistrue;identity_recoverediffidentity.resolution_methodislocalstorage_recoveryorsessionstorage_recovery;attribution_touchiffattribution.is_new_touchistrue;consent_changeiff theconsentcomponent 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).
conversion
Section titled “conversion”{ "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.
identify
Section titled “identify”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):
type∈email|phone|customer.email/phonemay use a plainsha256profile computed client-side;customermust usehmac_sha256with 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.algorithm∈sha256|hmac_sha256.key_versionis required iffhmac_sha256and prohibited forsha256; 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 }:state∈authenticated|verified;method∈authenticated_session|verified_link|verified_otp|payment;timeis Unix seconds.provenanceandtenant_binding: required strings, each 1–128 bytes.
Extensions
Section titled “Extensions”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.
_meta — the data-quality signal
Section titled “_meta — the data-quality signal”| Field | Type | Values |
|---|---|---|
_meta.uiaf_version | const | "3.1.1". |
_meta.data_quality | enum | full | stripped | restricted. |
_meta.attribution_completeness | enum | full | utm_only | request_context_only | none. |
_meta.emitter | enum | client | 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 permissionsallowed.stripped:analytics_storageallowedwith at least one ad permissiondenied(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.
Example Payloads
Section titled “Example Payloads”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" }}Byte ceilings
Section titled “Byte ceilings”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.
| Surface | Max UTF-8 bytes |
|---|---|
| Immutable wire/storage body | 32768 |
identify identifiers array | 8192 |
event_data.extensions object | 4096 |
| Whole touchpoint | 6144 |
click_ids object | 4096 |
custom object | 2048 |
Per custom value | 256 |
| Click-ID value | 1–512 |
Dirty projection / uiaf_dirty_baseline | 8192 |
| Pending-context object | 1024 |
| Pending-context member | 128 |
Size profiles
Section titled “Size profiles”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:
- If the standard body is ≤ 32768 bytes, send it byte-identical.
- Otherwise apply step 1 (
client → null); if it now fits, that is the compact body. - Otherwise apply step 2 (
last_touch.term,last_touch.content,last_touch.custom → null); if it now fits, that is the compact body. - 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.
Runtime IPC registration
Section titled “Runtime IPC registration”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).
| Name | Primitive | Role |
|---|---|---|
uiaf-session-alloc:<session_id> | Web Lock | Lifetime exclusive per-session sequence allocator lock. |
uiaf-session-claim | BroadcastChannel | Session advisory channel. |
uiaf-identity-issuance | Web Lock | Short-lived origin-wide lock held only around the authoritative re-read + sole-winner cookie-endpoint call + post-response cookie readback. |
uiaf-consent-wakeup | BroadcastChannel | Wake-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-drain | Web Lock | Retry-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.
Storage-key registry
Section titled “Storage-key registry”This section defers to uiaf-storage-keys.json; it never hand-maintains a competing list.
Retained (browser storage):
| Key | Mechanism | Contents |
|---|---|---|
uiaf_uid | cookie (server-set) + LS/SS mirror | Persistent {uuid_v4}.{unix_seconds}. |
uiaf_recovery | LS/SS only | Opaque server-bound recovery credential (never a cookie, never DSR auth). |
uiaf_session_state | sessionStorage | { session_id, next_seq } — the sole v3 session representation. |
uiaf_attribution | LS/SS | Attribution record (first/last touch, click-ID metadata, count). |
uiaf_retry_queue | localStorage | Retry entries keyed by immutable event_id. |
uiaf_dirty_baseline | sessionStorage | The 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).
Base persistence predicate
Section titled “Base persistence predicate”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.
Artifacts, versions & release ledger
Section titled “Artifacts, versions & release ledger”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 JSONnullfor the first.artifacts: exactly eight unique{ path, sha256 }records in lexicographic path order (the four artifact files under bothschema/<edition>/andschema/<channel>/), paths relative todocs-site/public,sha256lowercase 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.
Auxiliary control-plane interfaces
Section titled “Auxiliary control-plane interfaces”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.
Cross-domain handoff — _uiaf_token
Section titled “Cross-domain handoff — _uiaf_token”_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 & DSR — control plane only
Section titled “Revocation & DSR — control plane only”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).
Consent handling at the edges
Section titled “Consent handling at the edges”Pending-context allowlist (pre-consent buffer)
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.
Consent validation at the receiving endpoint
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
statusispending(the data plane admits no pending payload). - A payload where GPC is
detectedandapplicableyet any of the three ad-purposeeffectivevalues isallowed(a self-contradictory vector: GPC forces all three ad purposes todenied). - A
tiershorthand that is inconsistent with its derivation. Tier derives only from theeffectivevector, with astatusofnot_applicableoverriding to tier0; observedsignalsare 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.
Delivery Mechanisms
Section titled “Delivery Mechanisms”Primary — fetch() POST
Section titled “Primary — fetch() POST”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}Fallback — navigator.sendBeacon()
Section titled “Fallback — navigator.sendBeacon()”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.
Server-side forwarding
Section titled “Server-side forwarding”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.
Retry queue
Section titled “Retry queue”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 immutablerequired_permissions,purpose, ordestinationsare no longer satisfied by current capability (a downgrade drops only the newly-unsatisfied entries before their next send, never redacted or resent under the sameevent_id). - Stale click bytes: a frozen body carrying any click-ID occurrence whose in-payload
expires_atsatisfiesnow >= expires_atat 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.
Endpoint Requirements
Section titled “Endpoint Requirements”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 a429as retryable and every other4xxas terminal; a429retry MUST resend the byte-identical frozen body under the sameevent_id, honoring aRetry-Afterhint 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 final3xxas 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_idwith 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 repeatedevent_idcarrying 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 toclientfor browser submissions), and only an authenticated server-to-server channel may assertemitter: "server". An unauthenticated ingress whose body assertsemitter: "server"MUST be hard-rejected with a4xx— 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-001 —
uiaf_uidMUST be server-minted only (there is no client persistent-UID fallback) and purged atomically withuiaf_recoveryon 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’sSet-Cookieon create and every browser write/retention of the atomicuiaf_uid/uiaf_recoverypair. 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-002 —
uiaf_recoveryMUST 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 withCache-Control: no-store. - UIAF-04-SESS-001 —
uiaf_session_stateMUST be{ session_id, next_seq }; a corrupt or oversize value MUST mint a new UUID-v4 session withnext_seq0. - UIAF-04-ATTR-001 —
uiaf_attributionMUST strip click-ID members on effectivead_storageloss and delete the whole record on analytics-permission loss, revocation, erasure, or dormant transition; storage loss mints, never reconstructs, touch identifiers. - UIAF-04-RETRY-001 —
uiaf_retry_queueentries MUST carry immutablerequired_permissions/purpose/destinationsand MUST be dropped (never redacted or resent under the sameevent_id) when those permissions are no longer satisfied. - UIAF-04-DIRTY-001 —
uiaf_dirty_baselineMUST 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-partyuiaf_uidcookie. When any already-validuiaf_uidcookie is present — whether it matches or diverges from server state — the route MUST emit noSet-Cookieand 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 with400and set no cookie; recover carries onlyuiaf_recoveryand additionally MUST satisfy the base persistence predicate and the deployment’s continuity policy. The endpoint MUST beOrigin-authoritative — a validOriginwith absentSec-Fetch-Siteaccepted, aSec-Fetch-Siteofcross-siterejected, and a missing or invalidOriginunable to establish same-origin — MUST accept strictapplication/jsononly, 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 establisheduid: nullsurvives. - 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.sendBeaconbody is sent cross-origin as non-safelistedapplication/json, the endpoint MUST explicitly handle the CORS preflight — answering theOPTIONSrequest with the appropriate CORS response headers — and MUST tolerate the beaconContent-Type; alternatively the producer MUST send a CORS-safelisted body that the endpoint resolves through content-negotiation.sendBeaconreturningtrueis 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 alongsidesendBeaconand MUST retain it until a2xx-acknowledged drain; asendBeacontrue/falsereturn MUST NOT remove or advance that entry, and MUST NOT on its own advanceuiaf_dirty_baselineor consume a current-documentcreate/recoverytrigger. 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-Lengthceiling — 40,960 bytes RECOMMENDED — before buffering or parsing the request body, rejecting over-limit requests with413without 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_queueentry MUST be the closed object{ payload, enqueued_at, attempts, next_attempt_at, bytes, required_permissions, purpose, destinations }with no additional members:payloadMUST 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_atMUST be integer Unix seconds in0..253402300799;attemptsMUST be an integer0..4294967295; andrequired_permissions/purpose/destinationsMUST 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/recoverytrigger MUST follow the state machinepending → in_flight → acknowledged | durably_enqueued | terminal | expired: a failed non-queued attempt returns it topendingunder 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 toterminaland cancel scheduled work; navigation or document destruction MUST move an unconsumed trigger toexpiredwith no cross-document replay; and exhausting the schedule without a2xxor durable enqueue MUST abandon the trigger as honest loss. - UIAF-04-REASON-001 — A
sessionpayload’sevent_data.reasonsMUST containsession_openiffidentity.session_startistrue,identity_creatediffidentity.is_newistrue,identity_recoverediffidentity.resolution_methodislocalstorage_recoveryorsessionstorage_recovery,attribution_touchiffattribution.is_new_touchistrue, andconsent_changeiff theconsentcomponent of the dirty projection differs from the supplied baseline where that comparison is available. When none of those five conditions holds but asessionis authorized by another dirty-projection delta,reasonsMUST 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
identitygroup MUST be internally coherent:resolution_methodofcookie,localstorage_recovery, orsessionstorage_recoveryrequires a non-nulluid;ephemeralrequiresuid: nullandconfidence: "low";is_new: truerequires a non-nulluidandresolution_method: "cookie";uid: nullrequiresis_new: false.confidenceMUST be derived —highonly for an authoritative cookie read, a fresh issuance, or a post-crosscheck credential-verified recovery;mediumonly for a credential-verified recovery adopted before the authoritative crosscheck;lowfor 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_recoverythe sole discriminator and any body UID400;200responses carry exactly{uid, recovery}— issuance withSet-Cookieand non-nullrecovery(a rotated credential supersedes its predecessor, which is refused thereafter), no-set with bothnulland noSet-Cookie; every denial is the non-enumerating403with exactly{"error": "denied"}; and the only statuses are200/400/403/429/5xx. The responseuidis 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-exposedfetch) whose body is exactly{ target, purpose, csrf_token }—targetan allowlisted exact clean absolute URL of 1–2048 bytes,purposethe constantidentity_handoff,csrf_tokena 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_tokenthat 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, orhistory.replaceStatefallback path. Only a successful mint (303with_uiaf_token) and a valid-target policy denial or internal mint failure (clean303without a token) MUST be redirects; a malformed or non-allowlistedtargetMUST be400, a same-origin/CSRF failure MUST be403, and a source rate limit MUST be429, none of which are redirects. - UIAF-04-AUX-005 — The handoff redeem (
POST /api/uiaf/handoff/redeem) MUST be server-to-server withContent-Type: application/jsonand a body of exactly{ token, target }, wheretargetis the exact clean absolute URL being handled (the destination URL without the_uiaf_tokenparameter); 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 return200 { uid }only on an exact binding match and otherwise400/401/403(any binding mismatch) or a non-enumerating410covering unknown, expired, and already-replayed/consumed tokens indistinguishably, with429/503as applicable. The target server MUST redeem before returning HTML or any subresource and issue a clean303before 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, orhistory.replaceStatefallback. 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 theuiaf_recoverycredential MUST NOT authenticate it.
Mock Code
Section titled “Mock Code”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
Section titled “Data Flow”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:
- Page load: the browser requests a page; the server reads HTTP headers (cookie, referrer, UTMs).
- Read consent: the app runs
readConsent()to resolve the effective vector (section 07). - While
pending: only the registered pending-context buffer is held, and nothing is sent, written, or persisted. - If
analytics_storageallowed: the app runsresolveIdentity()(reads/createsuid+session_statein cookie/localStorage) andcaptureAttribution()as post-resolution authorized capture (UTMs when authorized, click IDs only ifad_storageis allowed). Otherwise (analytics denied): no persistent identity (uidnull) and no automatic session. - Authorize the specific event:
authorizeEvent(context, consent)picks the specific discriminator with no default. - If
event == session(analytics allowed + documented purpose + [projection changed OR an unconsumedcreate/recoverytrigger]): build the dirty projection and compare it touiaf_dirty_baseline(or honor the pending one-shot trigger). Ifevent == conversion/identify(explicit basis): verify the event’s own authorization; the projection gate is bypassed. - Freeze: allocate the session sequence, then
assembleBody()andfreezeCanonical()(RFC 8785), only if an event was authorized. - Deliver: POST the frozen body (fetch or
sendBeacon) to the endpoint. - Endpoint: the shared reference validator runs, events are deduplicated by
event_idand ordered by(session_id, session_seq), then routed to downstream platforms by effective consent and_meta. - Baseline: advance
uiaf_dirty_baselineonly on a 2xx acknowledgement or durable enqueue, and only for asession.
Edge Cases
Section titled “Edge Cases”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.