Skip to content

Endpoint Schema


Five rules govern the payload format:

  1. One schema for all event types. Every payload — page view, consent update, identity link — uses the same top-level structure. The event field distinguishes them; the shape does not change. Identity initialization, recovery, and attribution capture are derived by the endpoint from payload fields, not sent as separate events.

  2. Endpoint-agnostic. The payload works with server-side GTM, a custom API, Snowplow, Tealium, or any system that accepts JSON over HTTP. No platform-specific fields at the UIAF layer. Platform-specific mapping (GA4 Measurement Protocol, Meta CAPI, Google Ads conversion import) is the endpoint’s responsibility.

  3. Self-describing. Every payload includes a _meta object that declares the data quality level, attribution completeness, identity resolution method, and UIAF version. A downstream consumer can inspect _meta without parsing the rest of the payload to decide how to route or filter it.

  4. Consent state always present in every payload. Whenever a payload is sent, the consent object is included. Downstream systems need consent state to make routing decisions — a Meta CAPI endpoint must know whether ad_user_data is granted before forwarding. At Tier 4 the system is dormant except for one final consent_update on transition (see Tier 4 section below), which also includes the consent object.

  5. Null means absent, not omitted. Fields that have no value are set to null, not removed from the payload. This distinguishes “we checked and there was nothing” from “we did not check.” A missing utm_term is null. A missing consent object would be a schema violation.


EventWhen FiredEmission
page_viewEvery page load or SPA navigationDefault — always sent
consent_updateUser changed consent settingsDefault — always sent on transition
identity_initNew UID generatedDerived — endpoint detects via identity.is_new == true
identity_recoverUID recovered from backup storageDerived — endpoint detects via identity.resolution_method containing _recovery
attribution_captureNew attribution parameters detectedDerived — endpoint detects via attribution.is_new_touch == true
identity_linkAnonymous UID linked to authenticated identityExplicit — sent by application code on login/signup

In the default model, a single page_view payload per page load carries all identity and attribution data. The endpoint derives identity_init (when identity.is_new == true), identity_recover (when resolution_method contains _recovery), and attribution_capture (when attribution.is_new_touch == true) from payload fields. Only consent_update and identity_link are sent as separate payloads.


{
"event": "page_view",
"timestamp": "2026-04-01T14:30:00.000Z",
"identity": {
"uid": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6.1647291600",
"session_id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890",
"is_new": false,
"resolution_method": "cookie",
"confidence": "high"
},
"consent": {
"tier": 0,
"gcm": {
"ad_storage": "granted",
"analytics_storage": "granted",
"ad_user_data": "granted",
"ad_personalization": "granted"
},
"cmp_platform": "cookiebot",
"gpc_signal": false
},
"attribution": {
"first_touch": {
"source": "google",
"medium": "cpc",
"campaign": "spring_sale",
"term": "running shoes",
"content": "ad_v2",
"click_ids": {
"gclid": "Cj0KCQjw84anBhCtARIsAISI-xfSUJmQ8Z..."
},
"referrer": "google.com",
"landing_page": "/products/shoes",
"timestamp": 1647291600
},
"last_touch": {
"source": "facebook",
"medium": "paid_social",
"campaign": "retargeting_q2",
"term": null,
"content": "carousel_v3",
"click_ids": {
"fbclid": "IwAR2F4-dbP0l7Mn1IawQQGCINEz..."
},
"referrer": "facebook.com",
"landing_page": "/products/shoes",
"timestamp": 1647982200
},
"count": 3,
"is_new_touch": true
},
"page": {
"url": "https://example.com/products/shoes",
"path": "/products/shoes",
"referrer": "https://google.com",
"title": "Running Shoes | Example Store"
},
"client": {
"user_agent": "Mozilla/5.0...",
"language": "en-US",
"viewport": "1920x1080",
"screen": "1920x1080"
},
"_meta": {
"uiaf_version": "2.0.0",
"data_quality": "full",
"attribution_completeness": "full",
"identity_method": "cookie"
}
}

FieldTypeRequiredDescription
eventstringyesEvent type. Default events: page_view, consent_update. Explicit event: identity_link. Derived event types (identity_init, identity_recover, attribution_capture) are detected by the endpoint from payload fields rather than sent as separate payloads
timestampstring (ISO 8601)yesUTC timestamp with millisecond precision. Always ISO 8601 format for unambiguous parsing across time zones
FieldTypeRequiredDescriptionPossible Values
identity.uidstringconditionalPersistent user identifier. Format: {uuid_v4}.{unix_timestamp}. Null at consent tiers where persistent identity is prohibitedUUID v4 + dot + Unix seconds, or null
identity.session_idstringyesEphemeral session identifier. UUID v4, generated per tab, stored in sessionStorageUUID v4
identity.is_newbooleanyesWhether this UID was generated on the current page loadtrue, false
identity.resolution_methodstringyesHow the UID was resolved on this page load. cookie: UID found in cookie (whether originally server-set [400d, ITP-resilient] or client-set [7d on Safari] is not distinguishable client-side)cookie, localstorage_recovery, sessionstorage_recovery, new, ephemeral
identity.confidencestringyesConfidence level of the identity resolutionhigh, medium, low

Confidence mapping:

  • high — UID found in cookie. The cookie may have been server-set (400d) or client-set (7d on Safari). High confidence because cookies are the primary storage mechanism.
  • medium — UID recovered from localStorage or sessionStorage. The primary cookie was lost.
  • low — New UID generated or ephemeral session-only ID. No history, no prior attribution data.
FieldTypeRequiredDescriptionPossible Values
consent.tierintegeryesUIAF consent tier (see section 07)0, 1, 2, 3, 4
consent.gcmobjectyesGoogle Consent Mode v2 signal statesObject with four string fields
consent.gcm.ad_storagestringyesPermission to use cookies for advertisinggranted, denied
consent.gcm.analytics_storagestringyesPermission to use cookies for analyticsgranted, denied
consent.gcm.ad_user_datastringyesPermission to send user data to Google for advertisinggranted, denied
consent.gcm.ad_personalizationstringyesPermission to use data for ad personalizationgranted, denied
consent.cmp_platformstringyesConsent management platform in usecookiebot, onetrust, didomi, custom, none
consent.gpc_signalbooleanyesWhether Global Privacy Control header was detectedtrue, false
FieldTypeRequiredDescription
attribution.first_touchobject or nullyesFirst marketing touchpoint ever recorded for this user. Null if no attribution has been captured
attribution.last_touchobject or nullyesMost recent marketing touchpoint. Null if no attribution has been captured
attribution.countintegeryesNumber of attribution-carrying visits recorded. 0 = no attribution captured (consistent with last_touch: null). 1+ = at least one attributed visit (consistent with last_touch being non-null)
attribution.is_new_touchbooleanyesWhether new attribution was captured on this page load. true = new UTMs or referrer detected. false = direct visit or no change. The endpoint uses this to derive attribution_capture events

Touchpoint fields (apply to both first_touch and last_touch):

FieldTypeRequiredDescription
sourcestringyesTraffic source from utm_source or referrer classification (see section 03)
mediumstringyesMarketing medium from utm_medium or referrer classification
campaignstring or nullyesCampaign name from utm_campaign. Null if absent
termstring or nullyesKeyword from utm_term. Null if absent
contentstring or nullyesAd variant from utm_content. Null if absent
click_idsobjectyesMap of click ID parameter names to values. Empty object {} if none captured or if stripped by consent tier
referrerstring or nullyesReferrer domain. Null if referrer was empty
landing_pagestringyesURL path only (no query string) where the touchpoint was captured. Query strings are excluded to prevent PII leakage — unlike page.url, which captures the current page context, landing_page is stored long-term in attribution data
timestampintegeryesUnix timestamp (seconds) when the touchpoint was recorded
FieldTypeRequiredDescription
page.urlstringyesFull page URL including protocol and path, query string stripped
page.pathstringyesURL path only
page.referrerstring or nullyesFull referrer URL. Null if empty
page.titlestringyesDocument title
FieldTypeRequiredDescription
client.user_agentstringyesNavigator user agent string
client.languagestringyesNavigator language preference
client.viewportstringyesViewport dimensions as {width}x{height}
client.screenstringyesScreen dimensions as {width}x{height}

📡 _meta Object — The Data Quality Signal

Section titled “📡 _meta Object — The Data Quality Signal”

This is the payload’s self-description. Every downstream system reads _meta first to determine what the rest of the payload contains and how much it can be trusted.

FieldTypeRequiredDescriptionPossible Values
_meta.uiaf_versionstring (semver)yesUIAF specification version that produced this payload"2.0.0"
_meta.data_qualitystringyesOverall data completeness level"full", "stripped", "session_only", "none"
_meta.attribution_completenessstringyesWhat attribution data is present"full", "utm_only", "server_side_utm_only", "none"
_meta.identity_methodstringyesHow the UID was resolvedSame values as identity.resolution_method
  • full — All systems operational. Persistent UID, full attribution (UTMs + click IDs), complete page and client context. Consent tier 0 or 1.
  • stripped — Persistent UID present, UTM parameters captured, but click IDs removed from the attribution payload because they are personal data and consent for advertising storage was not granted. Consent tier 2.
  • session_only — No persistent identity. Ephemeral session ID only. No attribution data persisted across sessions. Consent tier 3.
  • none — Consent revoked. This value appears only on the final consent_update event when transitioning to Tier 4. No further payloads follow.
  • full — UTM parameters and click IDs are both present (where the URL contained them).
  • utm_only — UTM parameters captured. Click IDs excluded from the payload per consent constraints. The click IDs may still be present in the URL for Google Consent Mode and platform tags to read independently.
  • server_side_utm_only — UTM parameters captured server-side from the HTTP request (no device storage). Used at Tier 3 when Approach B (07-consent-integration.md) is implemented. Attribution data comes from server-injected template context, not client storage.
  • none — No attribution data in the payload. Either the visit carried no attribution parameters, or consent prohibits capturing them.

A GA4 endpoint receiving a payload with data_quality: "stripped" knows the UID is reliable for session stitching but the attribution object lacks click IDs for conversion import. A Meta CAPI endpoint receiving data_quality: "session_only" knows the identity is ephemeral and not suitable for user matching. A reporting dashboard receiving data_quality: "full" knows every field can be trusted at face value.

Without _meta, every downstream system must independently infer data completeness from field presence. An empty click_ids object could mean “user arrived direct with no ads” or “click IDs were stripped for privacy.” _meta eliminates the ambiguity.


All systems operational. Persistent identity, full attribution, complete payload.

{
"event": "page_view",
"timestamp": "2026-04-01T14:30:00.000Z",
"identity": {
"uid": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6.1647291600",
"session_id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890",
"is_new": false,
"resolution_method": "cookie",
"confidence": "high"
},
"consent": {
"tier": 1,
"gcm": {
"ad_storage": "granted",
"analytics_storage": "granted",
"ad_user_data": "granted",
"ad_personalization": "granted"
},
"cmp_platform": "cookiebot",
"gpc_signal": false
},
"attribution": {
"first_touch": {
"source": "google",
"medium": "cpc",
"campaign": "spring_sale",
"term": "running shoes",
"content": "ad_v2",
"click_ids": { "gclid": "Cj0KCQjw84anBhCtARIsAISI-xfSUJmQ8Z..." },
"referrer": "google.com",
"landing_page": "/products/shoes",
"timestamp": 1647291600
},
"last_touch": {
"source": "facebook",
"medium": "paid_social",
"campaign": "retargeting_q2",
"term": null,
"content": "carousel_v3",
"click_ids": { "fbclid": "IwAR2F4-dbP0l7Mn1IawQQGCINEz..." },
"referrer": "facebook.com",
"landing_page": "/products/shoes",
"timestamp": 1647982200
},
"count": 3,
"is_new_touch": true
},
"page": {
"url": "https://example.com/products/shoes",
"path": "/products/shoes",
"referrer": "https://google.com",
"title": "Running Shoes | Example Store"
},
"client": {
"user_agent": "Mozilla/5.0...",
"language": "en-US",
"viewport": "1920x1080",
"screen": "1920x1080"
},
"_meta": {
"uiaf_version": "2.0.0",
"data_quality": "full",
"attribution_completeness": "full",
"identity_method": "cookie"
}
}

Tier 2: Stripped — Analytics Without Advertising

Section titled “Tier 2: Stripped — Analytics Without Advertising”

Persistent identity retained. UTMs captured. Click IDs excluded from the payload because they are personal data requiring advertising consent. The click_ids object is empty, and _meta signals the reduction.

{
"event": "page_view",
"timestamp": "2026-04-01T14:30:00.000Z",
"identity": {
"uid": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6.1647291600",
"session_id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890",
"is_new": false,
"resolution_method": "cookie",
"confidence": "high"
},
"consent": {
"tier": 2,
"gcm": {
"ad_storage": "denied",
"analytics_storage": "granted",
"ad_user_data": "denied",
"ad_personalization": "denied"
},
"cmp_platform": "cookiebot",
"gpc_signal": false
},
"attribution": {
"first_touch": {
"source": "google",
"medium": "cpc",
"campaign": "spring_sale",
"term": "running shoes",
"content": "ad_v2",
"click_ids": {},
"referrer": "google.com",
"landing_page": "/products/shoes",
"timestamp": 1647291600
},
"last_touch": {
"source": "facebook",
"medium": "paid_social",
"campaign": "retargeting_q2",
"term": null,
"content": "carousel_v3",
"click_ids": {},
"referrer": "facebook.com",
"landing_page": "/products/shoes",
"timestamp": 1647982200
},
"count": 3,
"is_new_touch": true
},
"page": {
"url": "https://example.com/products/shoes",
"path": "/products/shoes",
"referrer": "https://google.com",
"title": "Running Shoes | Example Store"
},
"client": {
"user_agent": "Mozilla/5.0...",
"language": "en-US",
"viewport": "1920x1080",
"screen": "1920x1080"
},
"_meta": {
"uiaf_version": "2.0.0",
"data_quality": "stripped",
"attribution_completeness": "utm_only",
"identity_method": "cookie"
}
}

Tier 3: Session Only — No Persistent Identity

Section titled “Tier 3: Session Only — No Persistent Identity”

No persistent UID. An ephemeral session ID (generated fresh, stored in sessionStorage only) is the sole identifier. No attribution data persisted across sessions. Server-side HTTP header processing (referrer, UTMs from the URL) is permitted because reading request headers does not trigger ePrivacy Article 5(3) — no information is stored on or read from the user’s device.

{
"event": "page_view",
"timestamp": "2026-04-01T14:30:00.000Z",
"identity": {
"uid": null,
"session_id": "e7f8a9b0-1c2d-3e4f-5a6b-7c8d9e0f1a2b",
"is_new": true,
"resolution_method": "ephemeral",
"confidence": "low"
},
"consent": {
"tier": 3,
"gcm": {
"ad_storage": "denied",
"analytics_storage": "denied",
"ad_user_data": "denied",
"ad_personalization": "denied"
},
"cmp_platform": "cookiebot",
"gpc_signal": true
},
"attribution": {
"first_touch": null,
"last_touch": null,
"count": 0,
"is_new_touch": false
},
"page": {
"url": "https://example.com/products/shoes",
"path": "/products/shoes",
"referrer": "https://google.com",
"title": "Running Shoes | Example Store"
},
"client": {
"user_agent": "Mozilla/5.0...",
"language": "en-US",
"viewport": "1920x1080",
"screen": "1920x1080"
},
"_meta": {
"uiaf_version": "2.0.0",
"data_quality": "session_only",
"attribution_completeness": "none",
"identity_method": "ephemeral"
}
}

At tier 4, UIAF does not execute on page load. No identity resolution, no attribution capture, no page_view payloads.

Exception: the final consent_update. When transitioning TO tier 4 (e.g., user revokes all consent), one final consent_update event is sent with consent.tier: 4 and the current identity (the last time it will be transmitted). After that payload, the system goes fully dormant and sends nothing further. This final event lets the endpoint know the user revoked consent and should be excluded from further processing.

After the transition payload, from the endpoint’s perspective, the user ceases to produce data. No page_view, no attribution_capture, no identity_link — only that single consent_update marking the shutdown.


The default delivery method. Sends a JSON payload via HTTP POST with full request/response lifecycle.

function sendViaFetch(endpoint_url, payload) {
response = fetch(endpoint_url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload),
keepalive: true
})
if response.status != 200 AND response.status != 204 {
queueForRetry(payload)
}
}

The keepalive: true flag ensures the request completes even if the user navigates away before the response arrives. This is critical for single-page applications where route changes would otherwise abort in-flight requests.

Used during page unload (visibilitychange with visibilityState === "hidden", or pagehide event). sendBeacon is fire-and-forget: no response is returned, no retry is possible.

function sendViaBeacon(endpoint_url, payload) {
blob = new Blob(
[JSON.stringify(payload)],
{ type: "application/json" }
)
success = navigator.sendBeacon(endpoint_url, blob)
if not success {
// Beacon queue full or payload too large (64KB limit)
queueForRetry(payload)
}
}

Use sendBeacon only when fetch with keepalive is not available or when the browser is in the process of unloading the page. sendBeacon has a 64KB payload limit and no response handling.

For returning consented users on server-rendered pages, the application server can POST the payload directly to the endpoint. The server has the existing UID (from the cookie), UTMs and referrer (from the HTTP request), and consent state (from the CMP cookie). This path applies only when all three conditions are met — existing UID, verified consent, and a server-rendered response. It does NOT apply to first-time visitors (UID creation is client-side after consent), SPAs (navigation is client-driven), or pre-consent traffic.

This is the most resilient delivery method for returning users: no ad blocker, no browser restriction, server-to-server path. It supplements client-side delivery — it does not replace it.

When fetch fails (network error, non-2xx response) and sendBeacon is not applicable, queue the payload in localStorage under key uiaf_retry_queue as a JSON array. On the next page load, attempt to drain the queue before sending new events.

function queueForRetry(payload) {
queue = JSON.parse(localStorage.getItem("uiaf_retry_queue") OR "[]")
queue.push(payload)
// Cap queue at 50 entries to prevent localStorage bloat
if queue.length > 50 {
queue = queue.slice(-50) // Keep most recent
}
localStorage.setItem("uiaf_retry_queue", JSON.stringify(queue))
}
function drainRetryQueue(endpoint_url) {
queue = JSON.parse(localStorage.getItem("uiaf_retry_queue") OR "[]")
if queue.length == 0 { return }
for payload in queue {
sendViaFetch(endpoint_url, payload)
}
localStorage.removeItem("uiaf_retry_queue")
}
// CRITICAL: Clear retry queue on consent revocation
// Queued payloads from a higher consent tier must not survive a downgrade.
function clearRetryQueueOnRevocation() {
localStorage.removeItem("uiaf_retry_queue")
}
// This must be called in EVERY consent downgrade transition (see 07-consent-integration.md).

The receiving system (typically server-side GTM, but any HTTP endpoint) must handle the following:

  1. Accept POST with JSON body. Content-Type application/json. The payload is always a single JSON object, never an array or form-encoded data.

  2. Return 200 or 204 on success. The client uses the status code to determine whether to retry. Any 2xx is treated as success. 4xx means the payload was rejected (do not retry). 5xx means transient failure (retry on next page load).

  3. Handle CORS if browser-to-server. When the endpoint is on a different origin than the website (common with sGTM on a subdomain or separate domain), it must return appropriate CORS headers:

    • Access-Control-Allow-Origin: https://example.com (or the specific origin, never * for credentialed requests)
    • Access-Control-Allow-Methods: POST, OPTIONS
    • Access-Control-Allow-Headers: Content-Type
    • Respond to OPTIONS preflight requests with 204
  4. Process _meta to route and filter. The endpoint should inspect _meta.data_quality and _meta.attribution_completeness before forwarding to downstream platforms. A payload with attribution_completeness: "utm_only" should not be sent to a Meta CAPI endpoint that requires fbclid. A payload with data_quality: "session_only" should not be sent to a system expecting persistent user IDs.

  5. Idempotency. The retry mechanism may deliver the same payload twice. The endpoint should handle duplicates gracefully — either via deduplication (using timestamp + session_id as a natural key) or by accepting that downstream platforms handle their own deduplication.


function assemblePayload(context, consent) {
var event = context.event OR "page_view" // required: the event type for this payload
var identity = context.identity
var attribution = context.attribution OR { first_touch: null, last_touch: null, count: 0, is_new_touch: false }
var pre_consent = context.pre_consent // server-injected UTMs (Approach B)
var page = context.page OR getPageContext()
// Determine data quality based on consent tier
data_quality = "full"
attribution_completeness = "full"
if consent.tier == 2 {
data_quality = "stripped"
attribution_completeness = "utm_only"
// Strip click IDs from attribution touchpoints
if attribution.first_touch != null {
attribution.first_touch.click_ids = {}
}
if attribution.last_touch != null {
attribution.last_touch.click_ids = {}
}
}
if consent.tier == 3 {
data_quality = "session_only"
identity.uid = null
identity.resolution_method = "ephemeral"
identity.confidence = "low"
// T3 attribution: server-injected UTMs/referrer if Approach B is implemented
// Activate when ANY pre-consent data exists (UTMs OR referrer), not just utm_source
var has_pre_consent = pre_consent != null AND (
pre_consent.utms.source != null OR
pre_consent.utms.medium != null OR
pre_consent.utms.campaign != null OR
pre_consent.referrer != null
)
if has_pre_consent {
attribution_completeness = "server_side_utm_only"
attribution = {
last_touch: {
source: pre_consent.utms.source OR classifyReferrer(pre_consent.referrer).source,
medium: pre_consent.utms.medium OR classifyReferrer(pre_consent.referrer).medium,
campaign: pre_consent.utms.campaign, // null if absent
term: pre_consent.utms.term, // null if absent
content: pre_consent.utms.content, // null if absent
click_ids: {}, // never captured at T3
referrer: pre_consent.referrer, // null if empty
landing_page: pre_consent.landing_page,
timestamp: pre_consent.timestamp
},
first_touch: null, // no persistent first touch at T3
count: 1, // one attribution-carrying visit on this page load
is_new_touch: true
}
} else {
attribution_completeness = "none"
attribution = { first_touch: null, last_touch: null, count: 0, is_new_touch: false }
}
}
if consent.tier >= 4 {
data_quality = "none"
attribution_completeness = "none"
// T4 consent_update: identity from prior state, no attribution
attribution = { first_touch: null, last_touch: null, count: 0, is_new_touch: false }
}
payload = {
event: event,
timestamp: new Date().toISOString(),
identity: {
uid: identity.uid,
session_id: identity.session_id,
is_new: identity.is_new,
resolution_method: identity.resolution_method,
confidence: identity.confidence
},
consent: {
tier: consent.tier,
gcm: consent.gcm,
cmp_platform: consent.cmp_platform,
gpc_signal: consent.gpc_signal
},
attribution: attribution,
page: {
url: page.url,
path: page.path,
referrer: page.referrer,
title: page.title
},
client: {
user_agent: navigator.userAgent,
language: navigator.language,
viewport: window.innerWidth + "x" + window.innerHeight,
screen: screen.width + "x" + screen.height
},
_meta: {
uiaf_version: "2.0.0",
data_quality: data_quality,
attribution_completeness: attribution_completeness,
identity_method: identity.resolution_method
}
}
return payload
}
function sendToEndpoint(payload) {
endpoint_url = config.endpoint_url
// Drain any previously queued payloads first
drainRetryQueue(endpoint_url)
// Determine delivery method based on page lifecycle
if document.visibilityState == "hidden" {
sendViaBeacon(endpoint_url, payload)
} else {
sendViaFetch(endpoint_url, payload)
}
}

The key insight: consent constraints are applied at collection time, not assembly time. The client reads consent FIRST, then resolves identity and captures attribution only within permitted bounds. By the time assemblePayload runs, the data already reflects consent constraints — click IDs were never captured at T2, no persistent identity exists at T3. The _meta object describes the resulting data quality so the endpoint can route appropriately.


Payload size exceeds sendBeacon limit. The 64KB sendBeacon limit is generous for a single event payload (typical payloads are 1-3KB). If custom extensions push the payload beyond 64KB, fall back to fetch with keepalive: true, which has no such limit. If even that fails, truncate the client object first (lowest-value fields), then attribution term and content fields.

Single payload per page load. In the default model, a single page_view payload carries identity and attribution data. The endpoint can detect new visitors (identity.is_new), recoveries (resolution_method), and new attribution (attribution.is_new_touch) from payload fields without requiring separate event payloads. If your endpoint architecture requires discrete event types for separate processing pipelines, you can emit additional payloads — but this is an implementation choice, not a schema requirement.

Consent state changes mid-session. The user grants consent, then revokes it on the next page. The consent_update event fires with the new consent state. Subsequent payloads reflect the new tier. Already-sent payloads are not retroactively modified — the endpoint received them under the consent state that was active at the time.

Clock skew between client and server. The timestamp is generated client-side. Client clocks can be wrong. The endpoint should record its own receipt timestamp for ordering and deduplication. The client timestamp is useful for latency measurement and approximate event ordering, but should not be treated as authoritative for compliance timestamps.

Endpoint is unreachable. Network failure, DNS resolution failure, or endpoint downtime. The payload is queued in localStorage (max 50 entries). On the next successful page load, queued payloads are drained before new events are sent. If localStorage is also unavailable (private browsing, storage full), the payload is lost. This is acceptable — UIAF is analytics infrastructure, not a transactional system.