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 value | Meaning |
|---|---|
granted | An affirmative analytics choice collected by the storefront; usable only within its original cutoff and all remaining configuration, freshness, host, surface, and activation gates. |
denied | Refusal or withdrawal. Known denial survives lapse. |
unknown | No 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 path | Input / result |
|---|---|
POST /api/v1/public/checkout/sessions/{token}/analytics-consent | x-idempotency-key; body state, assertedAt, updateCapability, optional inheritedDenial; 201 handoff result. |
POST /api/v1/public/checkout/sessions/{token}/analytics-consent/updates | x-idempotency-key, x-consent-update-capability; body state, assertedAt, expectedRevision; 200 decision, including refused decisions. |
GET /api/v1/public/checkout/sessions/{token}/analytics-consent | Private current state and configuration; no update secret. |
GET /api/v1/public/checkout/sessions/{token}/analytics-consent/cleanup | Private 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 failure | Required handling |
|---|---|
superseded | A newer choice outranks this intent, or unknown would erase denial. Do not rebase an old grant. |
rejected_stale_revision | Re-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_newer | Assertion is not strictly newer than the accepted choice; do not forge a later timestamp. |
rejected_after_cutoff | A grant cannot extend the original ceiling. |
rejected_unknown_never_grants | No usable explicit grant; keep analytics suppressed. |
rejected_capability_unavailable | Wrong, 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 / 409 | Validation 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 / 5xx | Delivery 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-consent | Access and landed contract |
|---|---|
GET | Authenticated organization member; current safe status/reason/source/evaluation time and coarse trends. |
POST /enable | Owner/Admin; body acknowledgementVersion: "e32_adoption_v1", bound to the current attested measurement ID and attestation instant. Existing current enable is a no-op. |
POST /disable | settings:manage; adopted environments stay adopted and dark. A never-adopted environment returns 409 NOT_ADOPTED. |
GET /control-evidence | Owner/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.
| Class | Minimum fields, purpose and access | Lifetime / deletion |
|---|---|---|
| I1 — enforcement summary | Tenant/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 retry | Capability 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 page | Current 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 dedupe | Scoped 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 configuration | Adoption/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 — counts | Tenant/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 controls | Tenant/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 proof | Synthetic 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 copies | Only 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.