Endpoint Schema
📐 Design Principles
Section titled “📐 Design Principles”Five rules govern the payload format:
-
One schema for all event types. Every payload — page view, consent update, identity link — uses the same top-level structure. The
eventfield 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. -
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.
-
Self-describing. Every payload includes a
_metaobject that declares the data quality level, attribution completeness, identity resolution method, and UIAF version. A downstream consumer can inspect_metawithout parsing the rest of the payload to decide how to route or filter it. -
Consent state always present in every payload. Whenever a payload is sent, the
consentobject is included. Downstream systems need consent state to make routing decisions — a Meta CAPI endpoint must know whetherad_user_datais granted before forwarding. At Tier 4 the system is dormant except for one finalconsent_updateon transition (see Tier 4 section below), which also includes the consent object. -
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 missingutm_termisnull. A missingconsentobject would be a schema violation.
📋 Event Types
Section titled “📋 Event Types”| Event | When Fired | Emission |
|---|---|---|
page_view | Every page load or SPA navigation | Default — always sent |
consent_update | User changed consent settings | Default — always sent on transition |
identity_init | New UID generated | Derived — endpoint detects via identity.is_new == true |
identity_recover | UID recovered from backup storage | Derived — endpoint detects via identity.resolution_method containing _recovery |
attribution_capture | New attribution parameters detected | Derived — endpoint detects via attribution.is_new_touch == true |
identity_link | Anonymous UID linked to authenticated identity | Explicit — 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.
📦 Full Payload Schema
Section titled “📦 Full Payload Schema”{ "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" }}📖 Field Reference
Section titled “📖 Field Reference”Top-Level Fields
Section titled “Top-Level Fields”| Field | Type | Required | Description |
|---|---|---|---|
event | string | yes | Event 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 |
timestamp | string (ISO 8601) | yes | UTC timestamp with millisecond precision. Always ISO 8601 format for unambiguous parsing across time zones |
identity Object
Section titled “identity Object”| Field | Type | Required | Description | Possible Values |
|---|---|---|---|---|
identity.uid | string | conditional | Persistent user identifier. Format: {uuid_v4}.{unix_timestamp}. Null at consent tiers where persistent identity is prohibited | UUID v4 + dot + Unix seconds, or null |
identity.session_id | string | yes | Ephemeral session identifier. UUID v4, generated per tab, stored in sessionStorage | UUID v4 |
identity.is_new | boolean | yes | Whether this UID was generated on the current page load | true, false |
identity.resolution_method | string | yes | How 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.confidence | string | yes | Confidence level of the identity resolution | high, 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.
consent Object
Section titled “consent Object”| Field | Type | Required | Description | Possible Values |
|---|---|---|---|---|
consent.tier | integer | yes | UIAF consent tier (see section 07) | 0, 1, 2, 3, 4 |
consent.gcm | object | yes | Google Consent Mode v2 signal states | Object with four string fields |
consent.gcm.ad_storage | string | yes | Permission to use cookies for advertising | granted, denied |
consent.gcm.analytics_storage | string | yes | Permission to use cookies for analytics | granted, denied |
consent.gcm.ad_user_data | string | yes | Permission to send user data to Google for advertising | granted, denied |
consent.gcm.ad_personalization | string | yes | Permission to use data for ad personalization | granted, denied |
consent.cmp_platform | string | yes | Consent management platform in use | cookiebot, onetrust, didomi, custom, none |
consent.gpc_signal | boolean | yes | Whether Global Privacy Control header was detected | true, false |
attribution Object
Section titled “attribution Object”| Field | Type | Required | Description |
|---|---|---|---|
attribution.first_touch | object or null | yes | First marketing touchpoint ever recorded for this user. Null if no attribution has been captured |
attribution.last_touch | object or null | yes | Most recent marketing touchpoint. Null if no attribution has been captured |
attribution.count | integer | yes | Number 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_touch | boolean | yes | Whether 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):
| Field | Type | Required | Description |
|---|---|---|---|
source | string | yes | Traffic source from utm_source or referrer classification (see section 03) |
medium | string | yes | Marketing medium from utm_medium or referrer classification |
campaign | string or null | yes | Campaign name from utm_campaign. Null if absent |
term | string or null | yes | Keyword from utm_term. Null if absent |
content | string or null | yes | Ad variant from utm_content. Null if absent |
click_ids | object | yes | Map of click ID parameter names to values. Empty object {} if none captured or if stripped by consent tier |
referrer | string or null | yes | Referrer domain. Null if referrer was empty |
landing_page | string | yes | URL 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 |
timestamp | integer | yes | Unix timestamp (seconds) when the touchpoint was recorded |
page Object
Section titled “page Object”| Field | Type | Required | Description |
|---|---|---|---|
page.url | string | yes | Full page URL including protocol and path, query string stripped |
page.path | string | yes | URL path only |
page.referrer | string or null | yes | Full referrer URL. Null if empty |
page.title | string | yes | Document title |
client Object
Section titled “client Object”| Field | Type | Required | Description |
|---|---|---|---|
client.user_agent | string | yes | Navigator user agent string |
client.language | string | yes | Navigator language preference |
client.viewport | string | yes | Viewport dimensions as {width}x{height} |
client.screen | string | yes | Screen 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.
Fields
Section titled “Fields”| Field | Type | Required | Description | Possible Values |
|---|---|---|---|---|
_meta.uiaf_version | string (semver) | yes | UIAF specification version that produced this payload | "2.0.0" |
_meta.data_quality | string | yes | Overall data completeness level | "full", "stripped", "session_only", "none" |
_meta.attribution_completeness | string | yes | What attribution data is present | "full", "utm_only", "server_side_utm_only", "none" |
_meta.identity_method | string | yes | How the UID was resolved | Same values as identity.resolution_method |
data_quality Values
Section titled “data_quality Values”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 finalconsent_updateevent when transitioning to Tier 4. No further payloads follow.
attribution_completeness Values
Section titled “attribution_completeness Values”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.
Why This Matters
Section titled “Why This Matters”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.
🎚️ Example Payloads Per Consent Tier
Section titled “🎚️ Example Payloads Per Consent Tier”Tier 0 / Tier 1: Full Capability
Section titled “Tier 0 / Tier 1: Full Capability”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" }}Tier 4: Dormant — No Ongoing Payloads
Section titled “Tier 4: Dormant — No Ongoing Payloads”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.
🚀 Delivery Mechanisms
Section titled “🚀 Delivery Mechanisms”Primary: fetch() POST
Section titled “Primary: fetch() POST”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.
Fallback: navigator.sendBeacon()
Section titled “Fallback: navigator.sendBeacon()”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.
Server-Side Forwarding
Section titled “Server-Side Forwarding”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.
Retry Queue
Section titled “Retry Queue”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).✅ Endpoint Requirements
Section titled “✅ Endpoint Requirements”The receiving system (typically server-side GTM, but any HTTP endpoint) must handle the following:
-
Accept POST with JSON body. Content-Type
application/json. The payload is always a single JSON object, never an array or form-encoded data. -
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).
-
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, OPTIONSAccess-Control-Allow-Headers: Content-Type- Respond to
OPTIONSpreflight requests with 204
-
Process
_metato route and filter. The endpoint should inspect_meta.data_qualityand_meta.attribution_completenessbefore forwarding to downstream platforms. A payload withattribution_completeness: "utm_only"should not be sent to a Meta CAPI endpoint that requiresfbclid. A payload withdata_quality: "session_only"should not be sent to a system expecting persistent user IDs. -
Idempotency. The retry mechanism may deliver the same payload twice. The endpoint should handle duplicates gracefully — either via deduplication (using
timestamp+session_idas a natural key) or by accepting that downstream platforms handle their own deduplication.
💻 Mock Code
Section titled “💻 Mock Code”Payload Assembly
Section titled “Payload Assembly”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}Endpoint Delivery
Section titled “Endpoint Delivery”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) }}⚙️ Data Flow
Section titled “⚙️ Data Flow”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.
⚠️ Edge Cases
Section titled “⚠️ Edge Cases”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.