Skip to content

Storefront analytics consent handoff

Use this contract to report a storefront's analytics choice for one ordinary CART / ONE_TIME hosted checkout. It is independent of your framework or consent-management platform (CMP). Enforcement covers the platform-owned hosted Google Analytics integration only. You remain responsible for your storefront tags, CMP, consent capture, disclosures, and public-document handoff. Marketing/newsletter consent and payment authorization are separate.

Delivery status — 28 September 2026: the public API and dormant adoption backend landed in S2 #4298 and S3 #4432. The host binding correction #4472 landed in #4477. Hosted enforcement S4 #4262 remains blocked pending explicit child/queue resumption reconciliation; event handling S5 #4263 and the adoption UI S6 #4264 are incomplete. The requirements below for those surfaces are pending behavior. This guide does not establish Production readiness, enablement, activation, or browser/network verification. Opt-in storefronts must keep hosted analytics activation on hold. Follow the onboarding checklist before any separately authorized adoption.

Choice and lifetime

Wire valueMeaning
grantedAn affirmative analytics choice collected by the storefront; usable only within its original cutoff and all remaining configuration, freshness, host, surface, and activation gates.
deniedRefusal or withdrawal. Known denial survives lapse.
unknownNo usable choice. Omission, malformed input, an unusable read, or missing evidence never grants.

A malformed handoff can be rejected; it is not an accepted unknown record. Unavailable analytics consent must leave commerce available. Neither a Google cookie, account login, geography, linker, newsletter choice nor measurement ID proves a grant.

The initial handoff records one tenant/environment/original-session snapshot. The platform freezes the checkout's expiry at acceptance as originalCutoffAt. Repricing, activity, retries, reload, re-entry, and updates cannot move it. A paid transition alone is not withdrawal. The current reader reports a stored grant as unknown after cutoff, while retaining the original cutoff and ordered summary; a known denial stays denied.

A successor checkout needs its own fresh handoff, secret, and retry key. Reasserting a still-valid explicit storefront choice need not require another banner click, but copying the predecessor's hosted grant is insufficient. Carry known refusal explicitly as state: "denied", inheritedDenial: true; that flag is invalid with any other state. No predecessor lookup or automatic cross-device consent system is supplied, and no preference center is promised.

Landed public API

Paths below include /api/v1. Use the API origin for the intended environment from Environments. The opaque original checkout reference is the existing token credential. These public routes require neither a merchant bearer token nor x-organization-slug; updates additionally require the consent capability. A browser origin must satisfy the platform's existing CORS admission. Do not add consent state or the update secret to the hosted checkout URL, query, linker, or return URL.

Method and pathInput / result
POST /api/v1/public/checkout/sessions/{token}/analytics-consentx-idempotency-key; body state, assertedAt, updateCapability, optional inheritedDenial; 201 handoff result.
POST /api/v1/public/checkout/sessions/{token}/analytics-consent/updatesx-idempotency-key, x-consent-update-capability; body state, assertedAt, expectedRevision; 200 decision, including refused decisions.
GET /api/v1/public/checkout/sessions/{token}/analytics-consentPrivate current state and configuration; no update secret.
GET /api/v1/public/checkout/sessions/{token}/analytics-consent/cleanupPrivate minimum cleanup obligation; no update secret.

Use the current OpenAPI reference for full schemas. assertedAt is the actual storefront choice time, an ISO-8601 string with a timezone offset. Never replace it with retry time. expectedRevision is the current positive integer revision. Idempotency keys are nonblank and at most 200 characters. Keep the same key and identical body for a transport retry; new requests need new keys.

Generate and persist before sending

Generate an independent secret for each checkout: literal n, followed by at least 32 CSPRNG bytes encoded as base64url (43 characters) or hex (64). The API validates format, not randomness. The platform stores a hash, never returns the secret, and cannot recover it. Do not use the checkout token, customer identity, predictable input, or padding as the random material.

This JavaScript is an integration example, not a storage SDK. privateStore is your protected, tenant/environment/session-scoped adapter: save must durably finish before resolving; load must return the same pending operation on retry. Protect it from unrelated users, scripts, telemetry and logs. Prefer server custody when your storefront supports it. A browser implementation must explicitly address script access and loss/re-entry; this example does not prescribe a browser-storage key or a new retention permission. Do not forward the update secret to hosted checkout. Preserve it only for the necessary update/retry lifecycle, and define safe disposal and lost-secret handling.

// Inputs are placeholders/adapters supplied by your storefront.
// apiBase example: "https://api.example.invalid/api/v1" (never a live target).
function consentUrl(apiBase, checkoutToken, suffix = "") {
  return `${apiBase}/public/checkout/sessions/${encodeURIComponent(checkoutToken)}/analytics-consent${suffix}`;
}
 
async function prepareHandoff(privateStore, scope, choice) {
  const existing = await privateStore.load(scope);
  if (existing) return existing; // Reuse the original secret, key, and body.
  const bytes = crypto.getRandomValues(new Uint8Array(32));
  const secret = "n" + Array.from(bytes, b => b.toString(16).padStart(2, "0")).join("");
  const operation = {
    key: crypto.randomUUID(),
    body: {
      state: choice.state,
      assertedAt: choice.assertedAt,
      updateCapability: secret,
      ...(choice.inheritedDenial === true ? { inheritedDenial: true } : {}),
    },
  };
  await privateStore.save(scope, operation);
  return operation;
}
 
async function sendHandoff(apiBase, checkoutToken, operation) {
  const response = await fetch(consentUrl(apiBase, checkoutToken), {
    method: "POST",
    headers: { "Content-Type": "application/json", "x-idempotency-key": operation.key },
    body: JSON.stringify(operation.body),
    cache: "no-store", credentials: "omit", redirect: "error", referrerPolicy: "no-referrer",
  });
  if (response.status !== 201) return {
    delivery: "unacknowledged",
    status: response.status,
    retryable: response.status === 429 || (response.status >= 500 && response.status < 600),
  };
  return { delivery: "acknowledged", result: await response.json() };
}

Serialize preparation per scoped checkout so concurrent tabs/processes cannot create different initial secrets. If persistence fails, do not send. If sending times out or JSON cannot be read, keep the operation pending and retry with bounded backoff within the available seven-day replay window. For an HTTP failure, inspect status and retryable: only 429 or 5xx permits an automatic retry with backoff. Stop automatic retries on 400/404/409 and correct the cause; an expired receipt's 400 or a changed body's 409 cannot recover through repeated identical sends. Preserve the protected persisted operation for diagnosis; do not regenerate its secret or change its key/body merely to retry. Avoid tight retry loops, respect rate limits, and never log request URLs, headers, bodies or raw errors containing them. Commerce continues even when consent handoff fails. Once acknowledged, keep the capability separately from pending-operation state and process later choices through the update API.

The 201 result includes state, handoffAt, revision, grantGeneration, originalCutoffAt and updateCapabilityExpiresAt, not the capability. handoffAt dates evaluation of the returned state; it is not the durable acceptance timestamp or shopper choice time. A delayed response is not a fresh grant. Replays report the recorded acceptance without minting a secret or renewing either deadline; use the current reader before acting on state.

The capability expires 72 hours from first handoff acceptance, independently of the original grant cutoff. A new withdrawal can be accepted after cutoff while this capability remains valid. After capability expiry, a new withdrawal on that authority is impossible; null expiry means retired, never unlimited. The read/cleanup lifecycle can continue. Handoff retry receipts last at most seven days: identical retries are safe no-ops during that window; after receipt expiry, a second handoff is refused 400. A different secret/body under the same key is a 409 conflict. Neither window is permission to prolong grant use.

Withdraw and interpret the decision

Persist each update's retry key and exact body before sending, alongside the protected capability and existing scoped checkout reference. For example, pending.body can be the following schema-valid synthetic choice (the date must come from the real choice in an integration):

{
  "state": "denied",
  "assertedAt": "2026-09-28T12:11:00.000Z",
  "expectedRevision": 1
}
async function sendWithdrawal(apiBase, checkoutToken, capability, pending) {
  const response = await fetch(consentUrl(apiBase, checkoutToken, "/updates"), {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-idempotency-key": pending.key,
      "x-consent-update-capability": capability,
    },
    body: JSON.stringify(pending.body),
    cache: "no-store", credentials: "omit", redirect: "error", referrerPolicy: "no-referrer",
  });
  if (response.status !== 200) return {
    delivery: "unacknowledged",
    status: response.status,
    retryable: response.status === 429 || (response.status >= 500 && response.status < 600),
  };
  const result = await response.json();
  // HTTP 200 also carries refusals. Only this outcome acknowledges this choice.
  return {
    delivery: result.outcome === "accepted" ? "acknowledged" : "refused",
    result,
  };
}

outcome: "accepted" acknowledges a durable decision; replayed: true reports the original decision, not a new acceptance time. It does not prove a browser has stopped or Google has received anything. A replay is not permission to overwrite a later current state. A timeout/lost reply is pending or failed delivery, even if the platform may have committed; recover through an identical retry within the available window. Do not tell the shopper withdrawal completed merely because they clicked or received a 2xx status.

Update outcome / HTTP failureRequired handling
supersededA newer choice outranks this intent, or unknown would erase denial. Do not rebase an old grant.
rejected_stale_revisionRe-read current state. Only a still-applicable explicit choice may be resubmitted with its original choice time, a current revision and a new key. Never silently upgrade an old grant over withdrawal.
rejected_not_newerAssertion is not strictly newer than the accepted choice; do not forge a later timestamp.
rejected_after_cutoffA grant cannot extend the original ceiling.
rejected_unknown_never_grantsNo usable explicit grant; keep analytics suppressed.
rejected_capability_unavailableWrong, expired or retired capability. Returned unknown / null revision is a non-disclosing sentinel, not proof of the stored choice; no secret recovery is provided.
400 / 404 / 409Validation failure, neutral unavailable reference, or key reused for a different request. None acknowledges this withdrawal. Correct the cause; never blindly regenerate the handoff secret.
429 / network failure / 5xxDelivery unacknowledged; bounded backoff and truthful pending/failure reporting. Admission is source-based before token resolution.

A demonstrably newer explicit affirmative choice may restore a grant only before the immutable cutoff and under valid authority. It advances the grant generation; it never resurrects work from an earlier generation or backfills events. Update receipts last at most seven days from the decision: the matching capability can replay its own unexpired receipt even after its 72-hour validity ends, but retirement clears that proof and prevents replay. The replay returns the original outcome alongside the current effective state and revision. Retry-receipt expiry does not reset revision, denial or cutoff.

Private current-state and cleanup readers

async function readConsent(apiBase, checkoutToken) {
  const response = await fetch(consentUrl(apiBase, checkoutToken), {
    cache: "no-store", credentials: "omit", redirect: "error", referrerPolicy: "no-referrer",
  });
  if (!response.ok) return null; // Unusable evidence: suppress, never guess.
  return await response.json();
}

Both GETs return Cache-Control: private, no-store and Pragma: no-cache. They do not reconcile payment, reprice, create/resume a checkout, terminalize commerce, or renew consent. Do not substitute the ordinary commerce-session GET for them. Missing/ineligible authority gives an empty state projection or { "cleanupRequired": false, "cleanupRequiredAt": null }; some underlying lookup failures can still fail the request. A neutral result or error is not proof that cleanup succeeded.

The state response supplies readAt, revision, grantGeneration, originalCutoffAt, configurationRevision, and analyticsConfiguration. For adopted analytics, state: "granted" and configuration { policy: "adopted", permitted: true } are necessary, not sufficient. Null configuration suppresses. policy: "legacy", permitted: false says nothing about whether legacy analytics runs. The configuration marker supports equality only; it is not a tenant identity or a sequence number.

Hosted consumption remains pending: the token-only BYO reader shown above does not independently bind a token to the tenant resolved from the hosted request. The separate hosted binding correction landed in #4477; S4 must still consume and verify that contract. Do not use equal configuration markers as proof of tenant binding or present this BYO example as the final S4 reader. The BYO cleanup response contains only cleanupRequired and cleanupRequiredAt; it publishes no cookie allowlist, commerce DTO, or failure detail. Compare final hosted consumption against the landed correction as part of the required S4 closeout check.

Pending hosted enforcement requirements

The following is the accepted S4/S5 contract, not shipped enforcement. Initial entry, reload, re-entry, and resume must establish current usable, scoped consent and configuration before analytics runs. Active polling is selected at ten seconds with leases of at most twenty seconds, capped by the original cutoff. Use server readAt with conservative latency accounting; a delayed response cannot gain a fresh full lease on arrival. Reject stale, out-of-order, mismatched or failed reads and check deadlines before dispatch.

An accepted withdrawal must stop new hosted analytics requests on an active page within thirty seconds of durable platform acceptance; received denial acts immediately. The bound also covers accepted disable, ID change/clear, and invalidated acknowledgement. Loading and already-loaded tags, platform commands, and unsent work must all stop. Unknown/denied means tag and request suppression, including no denied-mode pings. An unacknowledged storefront update is outside this acceptance-to-stop bound: if hosted reads continue to succeed, the old grant may remain usable until delivery succeeds or its original cutoff expires. There is no click-to-stop deadline.

Hidden/background/inactive pages suspend analytics and obtain fresh authority before resuming. Offline/failed current reads suppress; re-entry cannot reuse an expired lease. Already transmitted data cannot be recalled.

Only existing sanitized page_view, begin_checkout and one-time purchase remain eligible. Prove grant at the underlying occurrence and again before send. First observing server CONFIRMED does not prove purchase-time consent: omit an event when that evidence is unavailable. Eligible unsent events can wait at most thirty seconds in the same active document and uninterrupted grant, shortened by cutoff. Discard on grant/configuration/freshness loss, inactivity or exit. No synthetic replay of missed page/checkout starts, no durable event queue, and no retry after dispatch merely because receipt is unknown. The order ledger stays authoritative.

Withdrawal cleanup and expired returns

Suppression must not wait for cookie deletion. On accepted withdrawal, attempt only explicitly owned, reachable hosted GA cookies; report bounded success, partial success, or failure. Storefront cookies, other domains, browser-blocked storage, and provider-held historical data are outside that attempt. Essential checkout and consent records remain. Unknown or ordinary grant lapse alone does not create withdrawal-specific cleanup.

If the page closed before cleanup, return through the same original reference must support the attempt even ten days later when commerce reports expired/404, while original resolution remains authorized within its separate retention. The unavailable shell must not mount GA, emit events, restore commerce or renew the grant. Only existing primary or already recognized, still-authorized resume references for that original are eligible. Legitimate deletion, revocation, retirement, excluded surfaces, wrong scope, or lost authorized resolution produce no authorized instruction; no identity search or restored binding is promised. One browser's success cannot erase the shared withdrawal or establish that all browsers are clear. S4 must prove the exact cookie allowlist and these failure paths; this guide supplies no hidden cleanup data or new old-ID store.

Adoption, disclosures and diagnostics

The landed adoption backend is dormant. It rejects enable with 503 CHECKOUT_ANALYTICS_ADOPTION_UNAVAILABLE until separately authorized platform availability. This document is not an instruction to change that gate. Preview and Production have separate configuration, adoption and evidence.

Merchant API under /api/v1/merchant/checkout-analytics-consentAccess and landed contract
GETAuthenticated organization member; current safe status/reason/source/evaluation time and coarse trends.
POST /enableOwner/Admin; body acknowledgementVersion: "e32_adoption_v1", bound to the current attested measurement ID and attestation instant. Existing current enable is a no-op.
POST /disablesettings:manage; adopted environments stay adopted and dark. A never-adopted environment returns 409 NOT_ADOPTED.
GET /control-evidenceOwner/Admin; bounded new control evidence, separate from existing settings audit history.

These merchant routes require the existing merchant authentication and tenant context. Their environment comes from deployment, not a caller-selected plane. ID management retains its existing permissions and Owner/Admin disclosure attestation. A changed ID, including A → B → A, requires renewed adoption acknowledgement and applicable integration validation. S6 owns the unfinished UI; no button or settings workflow is promised here.

For explicitly adopted environments, the accepted policy permits analytics_storage: granted in all regions only with usable affirmative consent and all other gates. ad_storage, ad_user_data, ad_personalization and advertising signals stay denied. Unknown/denied suppresses requests; Consent Mode's cookieless measurement is not a substitute. Disable/invalidated adoption stays dark, never falling back to legacy defaults. Environments that never adopted retain legacy DR-2265-03, including its regional Consent Mode posture, until an authorized transition. The scoped adopted policy does not fully supersede that legacy record. DR-1828-03 remains separate Production tag-activation authority.

Public-document handoff

Before adoption, publish tenant privacy disclosures describing the hosted GA events and data, the storefront's choice capture/update route, the adopted all-region policy, withdrawal acceptance versus delivery failure, the active thirty-second bound, expiry, cleanup limits and retention below. State the actual rollout status; do not publish pending safeguards as currently operating. Link the tenant privacy policy and hosted /cookies notice visibly from the storefront handoff. The platform notice is factual disclosure, not consent or a substitute for the tenant's policy. No automatic cross-device revocation or preference center is supplied. Keep notice text synchronized with delivered behavior and the qualified Owner/Admin acknowledgement.

Diagnostics describe platform decisions, not shoppers or provider receipt. Read failures yield explicit unknowns, not a clean bill of health. The current trend response covers today plus 28 preceding UTC days; counts can be lower bounds when observation recording fails. They do not prove Google collection, attribution continuity, or a browser's cleanup result.

Minimum data, access and retention

This is the selected I1–I9 / inventory B permission contract. A maximum is not a retention target, and it is not proof of provider configuration. Existing commerce retention, DR-2265 audits, legacy deduplication and source-owned protected QA receipts retain their separate obligations.

ClassMinimum fields, purpose and accessLifetime / deletion
I1 — enforcement summaryTenant/environment/session binding, initial/latest state, source/reason, revision/generation, original cutoff, latest choice/withdrawal times, configuration revision and minimal cleanup requirement. Service writes; scoped hosted projection only, no shopper-history UI.While the original is legitimately cleanup-addressable and obligations apply: no fixed-day maximum, potentially indefinite. Remove within 7 days after legitimate lifecycle end. Ordinary commerce expiry/404 alone does not start that clock. Never extend commerce retention.
I2 — capability and retryCapability hash/scope/validity; separate key hash, request digest, decision/revision/time. Consent service and holder's bounded response only; no raw secret, body, cookies or Google IDs in platform records.Verification facts only while update authority is needed; cleanup does not extend it. Landed new-update validity is 72 hours. Retry receipts at most 7 days from acceptance; receipt expiry cannot reset authority. Post-lifecycle cleanup within 7 days where applicable.
I3 — active pageCurrent revisions, lease/generation/visibility and minimal sanitized eligible unsent envelope/occurrence/deadline/dispatch guard; hosted document only.Memory only; lease ≤20 seconds, eligible wait ≤30 seconds, shortened by cutoff/loss/inactivity/exit. No retained polling history. Pending S4/S5.
I4 — adopted dedupeScoped opaque marker and expiry/dispatch fact; hosted browser logic only. No raw token, order/customer ID, amount or payload in new markers.Logical use ends at original cutoff; remove at next reachable access. Closed/offline browser residue has no fixed physical-deletion guarantee. Legacy markers unchanged. Pending S5.
I5 — current configurationAdoption/ever-adopted latch, enabled/revision, existing ID reference and acknowledgement/version; existing settings access and authenticated tenant/environment status.Existing tenant/integration lifecycle, no fixed-day maximum; no separate status history. Never expire the latch into legacy defaults.
I6 — countsTenant/environment/UTC day, fixed coarse reason categories and count; authorized tenant operators only. No shopper/session/order keys, amounts, IP, private URLs, free-text errors or new Google diagnostics.At most 30 days for every observation. Current backend reads 29-day buckets to leave purge margin.
I7 — new controlsTenant/environment, existing actor ID, action/time, config revision, acknowledgement/version and outcome; existing audit-reader roles. No shopper payload.At most 365 days from event; current backend expires at 364 days for purge margin. Existing audit obligations are separate.
I8 — synthetic proofSynthetic case ID, pinned code/environment/configuration revision, safe transitions, elapsed bounds, request classes/counts and redacted cleanup/deletion results; authorized Engineering/QA only.Temporary artifacts at most 30 days from capture. No credentials, raw cookie/storage dumps, private URLs or real shopper payloads. Durable signed conclusions remain source-owned.
I9 — recovery copiesOnly necessary copies of permitted I1/I2/I5/I6/I7 server records; tightly scoped recovery operators, no diagnostic or analytics reuse.At most 90 days from capture. Purge expired auxiliary data before use; fail closed until accepted authority is reconciled. Never recreate deleted authority.

Restore fencing #4283 and actual provider backup/PITR retention #4437 remain separate unresolved evidence/authority work. Do not infer either from a database migration, logical read filter or passing documentation test.