feat!: buyer consent with per-segment extensibility#451
Conversation
Reframe buyer_consent extension around a single primitive applied uniformly across all consent categories. Each category accepts either a boolean (blanket) or a per-segment map keyed by reverse-DNS identifiers, supporting channel-, program-, or purpose-specific opt-in (e.g., separate email vs. SMS marketing consent). Key changes: - consent_segment with description, links, allowed fields - description response-only via ucp_response: required + ucp_request: omit - allowed required in both directions (regime-neutral state field) - links uses common Link type with open type vocabulary - Composes onto buyer (cart + checkout) preserving each base's lifecycle - Segment identifiers governed by canonical reverse_domain_name type
jamesandersen
left a comment
There was a problem hiding this comment.
Another worthy alternate design alongside that from @amithanda on #407 ... this one going after all forms of consent. I can get behind this one also if we want to tackle the broader scope of consent (not just marketing) ... though honestly I'd recommend just dropping our pre-existing consent categories in the process if we go this route. @amithanda , @vixdug thoughts?
| }, | ||
| "allowed": { | ||
| "type": "boolean", | ||
| "description": "Current consent state. Businesses set the prior decision or default when advertising; platforms set the captured decision when confirming." |
There was a problem hiding this comment.
| "description": "Current consent state. Businesses set the prior decision or default when advertising; platforms set the captured decision when confirming." | |
| "description": "Current consent state. Businesses set the prior decision or jurisdiction-specific default when advertising; platforms set the captured decision when confirming." |
There was a problem hiding this comment.
Nit ... but want to suggest the idea that some jurisdictions should be modeled as opt-in, some as opt-out and it's the business' responsibility to own that decision
| "type": "object", | ||
| "description": "A single consent segment within a category. Businesses populate `description`, `links`, and current `allowed` state when advertising. Platforms populate `allowed` with the captured decision when confirming.", | ||
| "required": ["description", "allowed"], | ||
| "properties": { |
There was a problem hiding this comment.
If we were to add a dev.ucp.consent.mobile_app_push the business would need to collect some kind os specific push token that wouldn't be drawn from the existing cart/checkout request body. Should we consider some flexible "metadata" dict here e.g. businesses can look for additional platform derived data where applicable in this dict and report via messages if a needed token is not found / invalid.
I'm just pressure testing a bit here; this could also wait until the need is more clear
| "description": "A consent decision for a category. Either a boolean for blanket consent, or a map of per-segment consent for finer-grained capture (e.g., separate email vs. SMS opt-in).", | ||
| "oneOf": [ | ||
| { | ||
| "type": "boolean", |
There was a problem hiding this comment.
If this will be a breaking change anyway... do we have some conviction that:
- This alternate
boolmodeling is useful to keep around? - We've got the right four categories e.g.
analytics,marketing,preferences,sale_of_data? e.g. if introducing a more richly modeledconsent_segmentare these four categories really meaningful at all or should we dispense with this intermediate strict hierarchy and just let businesses express an array of consent segments? These four may have been inspired by some major regulation ... but we all know regulation changes / evolves frequently ;-) For implementors there could be some confusion trying to map to these four e.g. Google Tag platform models consents forad_storage,ad_user_dataandanalytics_data... are those allanalytics(because it's GTM) or are somemarketing(ads)?
There was a problem hiding this comment.
Discussed on #307 in response to @amithanda 's proposal a bit but I'd like to see "complete": "optional" here as well (or better understand why that would be problematic). In a common happy path a platform...
- Calls create checkout
- no business derived consent segments have been fetched so UX can't have rendered and collected buyer consent yet
- Is there a scenario where passing consent on
createmakes sense? ... maybe ifcartwas used first and it's assumed the consent segments are the same? Is that safe?
- renders checkout UX with a buyer's primary contact, shipping and payment information and applicable consent UX from the create response
- user reviews checkout details and interacts with the consent UX as applicable before confirming checkout
- Platform now calls
completecheckout providing the user's selections
If we continue to omit the buyer object on complete then this happy path is required to add an extra update network call to communicate the consent state. Can we avoid that latency? Does anything else that lives on buyer potentially alter the pricing totals in a way that would make this challenging?
There was a problem hiding this comment.
Is there a scenario where passing consent on create makes sense? ... maybe if cart was used first and it's assumed the consent segments are the same? Is that safe?
I could see another scenario where a platform that has default collection (email, sms) enabled and so when they make a request to create the checkout session, they also already have the consents collected.
But I agree, I think we would still want this to be available on complete though so that a two-shot, create -> complete for checkout_session could still be supported.
| | `dev.ucp.consent.sms` | SMS channel (any carrier) | | ||
|
|
||
| All other identifiers are governed by their owners under their own | ||
| reverse-DNS namespace — for example, `com.chatapp.marketing` for a |
|
Thanks for putting this together. Evolving I’d like to highlight 3 core architectural topics for us to discuss and align on, building upon some of 1. Polymorphism (Boolean vs. Segment Map)As
2. Consent Flow and Lifecycle (
|
Change buyer.ucp_request.complete from "omit" to "optional". This allows consent to be captured alongside / as a result of committing to complete the transaction.
|
@jamesandersen, @amithanda, @wsbrunson ty for the feedback! Updated to allow consent on The big design question is on nesting. I like the idea of a flat namespace and spent good chunk of time trying to iterate through how that would look. A key outcome and realization from running this exercise...
A consent decision is about a purpose (marketing, analytics, etc) that may be qualified by a channel / segment / vendor / program. One way or another, this relationship needs to be captured, and the question is by whom, as that determines where the grouping lives. Let's step through a few examples. Flat shape forces agent to own grouping"consent": {
"dev.ucp.consent.marketing.email": { ... },
"dev.ucp.consent.marketing.sms": { ... },
"com.chatapp.marketing": { ... },
"dev.ucp.consent.analytics": { ... },
"com.merchant.purpose_or_channel": { ... }
}Above yields a simple flat structure but, notably, it defers any semantics over purpose to the Channels / segments at top level are footgunsAs an example, (Recommended) nested shape: Purpose > (optional) SegmentsWe can normatively spec that businesses must provide "dev.ucp.consent.marketing": {
"allowed": false,
"description": "Promotional communication...",
"links": [...],
"segments": {
"dev.ucp.consent.marketing.email": { "allowed": true, "description": "...", links: [] },
"dev.ucp.consent.marketing.sms": { "allowed": false, "description": "...", links: [] },
"com.chatapp.channel.marketing": { "allowed": false, "description": "...", links: [] }
}
}The agent walks the tree. UI structure IS protocol structure: render purpose description, nest segments under with appropriate state and toggles. Channels / vendors nest naturally -- ChatApp owns the segment identifier, merchant does the work of logically grouping under a purpose key, which itself carries a description and links. No guessing for the agent required, and this mirrors structure that agents already model in their other channels and UIs. Further, the choice of how it's presented and group is material. In effect, protocol carries the grouping the user-facing UI needs, so the agent doesn't reconstruct it from heuristics. This structure yields roughly... "buyer": {
"consent": {
"dev.ucp.consent.marketing": {
"allowed": false,
"description": "Promotional communications across all channels",
"links": [...],
"segments": {
"dev.ucp.consent.marketing.email": { "allowed": true, "description": "Promotional emails and offers" },
"dev.ucp.consent.marketing.sms": { "allowed": false, "description": "Marketing text messages" },
"com.chatapp.channel.marketing": { "allowed": false, "description": "Marketing via ChatApp" }
}
},
"dev.ucp.consent.analytics": {
"allowed": true,
"description": "Site analytics and performance measurement",
"segments": {
"com.google.analytics": { "allowed": false, "description": "Google Analytics tracking" }
}
},
"dev.ucp.consent.preferences": { "allowed": true, "description": "Remember preferences and personalize experience" },
"dev.ucp.consent.sale_or_sharing": { "allowed": false, "description": "Sale or sharing of personal data with third parties" }
}
}UCP-curated "well-known" purposes:
Businesses are free to define own purpose buckets using rDNS convention, under which they slot optional segments. The top level contract of @jamesandersen's to your question of "are these 4 right?". AFAIK, unfortunately there is no universally agreed enumeration we can lean on. But this is an extensible list, so our job is not to enumerate exhaustive list but to spec a well-known subset, similar to how we do in many places across the spec with well-known codes, etc. Absence of an agreed standard/enumeration should not stop us from solving the common 80%. Thoughts, reactions? |
Evolves the existing
buyer_consentextension to support per-segment consent capture across all consent categories. Each category becomes polymorphic: a boolean for blanket consent, or a map of per-segment consent keyed by reverse-DNS identifiers. This design addresses the same regulatory motivations as #407 with a generic primitive that supports open extensibility for segments and consent categories without further schema changes.Each category accepts either a boolean (backward-compatible blanket form) or a per-segment map:
Existing
marketing: true | falsepayloads continue to work unchanged — the boolean form is permanent and first-class. Nonetheless, the composition is a breaking change because we also allow object format for segment breakdown.The same map shape carries both directions:
description,links, currentallowedallowedBusiness advertises in
create_checkout/update_checkoutresponse with the buyer's prior decision or the business's default for each segment. Platform captures the buyer's decisions and updates via:Per-segment data requirements (e.g.,
dev.ucp.consent.smsneedsbuyer.phone_number) are business-specific and not enforced by UCP. Missing dependencies surface via the standard message pattern:{ "messages": [ { "type": "warning", "code": "missing_consent_data", "content": "Phone number is required for SMS marketing.", "path": "$.buyer.phone_number" } ] }Checklist
!for breaking changes).