feat!: buyer consent with per-segment extensibility#451
Conversation
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?
| "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
|
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 (
|
|
@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? |
|
Hi @igrigorik, This nested 1. Enforcing Ordered Arrays for Consent Segments (Compliance)Should we design this as an array of objects for
2. Hierarchical Opt-In / Opt-Out Metadata (Jurisdictional Compliance)Different channels are governed by different regulations. For example, in the
Suggested Payload ShapesBusiness Advertise (Response)"buyer": {
"consent": {
"dev.ucp.consent.marketing": {
"allowed": true,
"description": "Promotional communications across all channels",
"links": [{"type": "privacy_policy", "url": "https://example.com/privacy"}],
"consent_model": "opt_out", // Fallback default for marketing segments
// Changed from a map to an array of objects to guarantee UI rendering order
"segments": [
{
"id": "dev.ucp.consent.marketing.email",
"allowed": true,
"description": "Promotional emails and offers",
"consent_model": "opt_out" // Inherited from parent; platform can pre-check
},
{
"id": "dev.ucp.consent.marketing.sms",
"allowed": false,
"description": "Marketing text messages",
"links": [{"type": "tcpa_disclosure", "url": "https://example.com/sms-terms"}],
"consent_model": "opt_in" // Overrides parent; platform must render unchecked
}
]
},
"dev.ucp.consent.sale_or_sharing": {
"allowed": true,
"description": "Do not sell or share my personal information",
"consent_model": "opt_out" // No segments; parent-level model is authoritative
}
}
}Platform Confirm (Request)When sending decisions back, the platform omits descriptive metadata "buyer": {
"consent": {
"dev.ucp.consent.marketing": {
"allowed": true, // User selected general marketing
"segments": [
{
"id": "dev.ucp.consent.marketing.email",
"allowed": true // User opted in to email
},
{
"id": "dev.ucp.consent.marketing.sms",
"allowed": false // User opted out of SMS
}
]
}
}
}3. Parent-Child Inheritance & Override Semantics (Prose Clarification)We should normatively specify how parent and child values interact in the spec
Let's discuss and take a call. |
|
@igrigorik / @amithanda I feel like this is close to the finish line and appreciate support from both of you! Here's my quick view:
Outstanding:
|
Replace the previous category/boolean consent shape with a nested Consent > Purpose > Segment model keyed by reverse-DNS identifiers. - make consent purpose and segment values uniform objects with `allowed` - advertise response-only `description` and `links` - allow buyer consent on checkout complete - define well-known consent purposes and marketing channel segments - document advertise/confirm flow, data dependencies, and segment override rules - update core concepts and playground capability metadata
7400301 to
a7145c7
Compare
|
@amithanda @jamesandersen updated the draft to capture the shape we reviewed above. PTAL. |
|
This looks great and would still solve the problems we had when we opened this proposal: #407 At the time we weren't sure how we could extend the marketing consent channels options to the other types of consents like analytics. What you have here works really well and would hopefully be the last breaking change to The one thing I'm unsure of: Does it make sense for Businesses to send an This would add more complexity to something that may or may not be an issue. Whatever we decide here, I'm good with this proposal 👍. |
|
@igrigorik I'm feeling really good about where this is headed. I just left a few smaller comments. The one slightly challenging area left (as per our discussion and @wsbrunson highlighting again) is how the platform is expected to handle Just for extreme clarity - is this how you expect a platform to interpret this in light of the drafted normative language requiring (i.e.
For both cases, unless for some reason the business needs to re-validate consent, it would omit these segments on subsequent cart/checkout transactions so the platform doesn't continually re-prompt for the same consent. This could be as a result of the business...
|
Replace single `allowed` boolean with `(checked, source)` pair to
distinguish business-defaults from platform-captured buyer decisions.
Resolves reviewer ambiguity about how to interpret an advertised value
without baking jurisdiction or regulatory framing into the protocol.
- Rename `allowed` → `checked`, mirroring the HTML input element. The
field is intentionally neutral: the value carries only binary state
and does not itself suggest a treatment, allowing the same shape to
model all forms of consent contracts
- Add `source: "business" | "platform"` to purpose and segment,
required in both directions
- Remove opt-in/opt-out and jurisdiction-specific references from
normative requirements; policy reasoning stays the business's
responsibility
- Replace error-message ambiguity on `complete_checkout` with an
explicit state-machine rule (MUST NOT transition to completed with
unmet consent dependencies); defer the mechanism to standard error
handling
- Establish full-replace confirm semantics consistent with the checkout
resource-replace pattern: `consent` field is optional, but when
submitted MUST include every advertised key with both `checked` and
`source`
- Add normatives for source attribution (requires buyer-stated
preference) and per-business scoping (decisions don't transfer across
businesses)
|
@jamesandersen @wsbrunson ty both. PTAL at the updated shape: e51a4fd. Taking a different approach and I believe it addresses your feedback cleanly. |
- Reframe clause 1: business advertises the complete set; platform
decides which choices to present and when; the MUST applies to
fidelity when presenting, not to presenting every advertised choice
- Drop `source` from clause 1's render list — it informs platform
decisions but is not user-facing content
- Scope the "opaque handles, MUST NOT infer semantics" guard to
identifiers outside the `dev.ucp.consent.*` namespace; UCP-defined
identifiers carry this spec's semantics so platforms can map them
to known buyer preferences
- Tighten source semantics: platforms MAY suppress re-presentation of
`source: "platform"` choices, removing the implication that the
full advertised set must be rendered every transaction
jamesandersen
left a comment
There was a problem hiding this comment.
nit only: "checked" seems a bit anchored in a particular UX representation ... would "consented" (or something similar?) work?
I think it is a good point,
|
Adds a teaching example before the comprehensive advertise example, showing the two key composition patterns side by side: purpose-level capture (analytics) and parent-with-segments override (marketing parent off, marketing.sms on per the buyer's consent). Per @jamesandersen review feedback on PR #451.
- Drop `additionalProperties: false` from `consent_purpose` and
`consent_segment` per the Schema Authoring Guide's open-objects
convention; keyed extensibility already lives on the parent maps
- Source semantics: `source: "platform"` MAY persist across
subsequent transactions with the same business
- Add clause 7 (Buyer review and revocation): platforms MUST provide
a way for buyers to audit and revoke or change prior consent
decisions
Addresses PR #451 review feedback from @amithanda.
Field-name choice is grammatical:
- `granted` is the past participle of a transitive verb and functions
natively as a state-adjective ("permission is granted", "consent is
granted") — exactly the function a boolean state field on a consent
record needs.
- `consented` is past-tense of an intransitive verb and reads as
event ("the buyer consented"), which creates a semantic
contradiction in `source: "business"` cells where no buyer action
occurred.
`granted` reads correctly across all four {state × source} cells.
The `source` field carries causation; `granted` carries the state.
|
@amithanda @jamesandersen updated, ptal! 🤞🏻 |
Clause 7 previously stated a free-floating `MUST provide a way for buyers
to audit and revoke prior consent decisions`, with the related `MAY
persist` and `MAY suppress re-presentation` discretions sitting in the
explanatory "Advertise and confirm" section. The two halves were not
visibly coupled, so the MUST read as a standalone-UI mandate.
Restructured as a conditional in one place:
- Platforms MAY persist prior buyer preferences and MAY suppress
re-presentation of unchanged values.
- Where preferences are persisted, platforms SHOULD give the buyer
the ability to change them.
|
Nice work on this, the extension reads much cleaner than the prior iterations. A few things I think turned out really well:
One side effect worth recording (which @jamesandersen's comment touched upon), not a blocker: because the Why I think this is fine: those fields are already submittable on Approving. Thanks for the careful iteration on this one. |
Evolves the
buyer_consentextension to support per-purpose and per-segment consent capture under a uniform object shape. Each consent decision carries the currentgrantedstate, thesourceidentifying who asserted it (business default vs. platform-captured buyer preference), and human-readable context — keyed by reverse-DNS identifiers at both purpose and segment levels. This generalizes the per-channel consent capture explored in #407 into a primitive that supports open extensibility for purposes and segments without further schema changes.Businesses advertise the current state of every supported consent option in cart/checkout responses:
This is a breaking change: the wire shape and field names differ from the prior
allowedboolean.grantedis a past-participle state-adjective ("consent is granted") that reads correctly in all four{state × source}combinations: business defaults and platform-captured buyer preferences alike. Thesourcefield carries causation;grantedcarries the state.The same map shape carries both directions:
granted,source,description; optionallinksandsegmentsgranted,source;segmentswhen presentPlatforms submit the current state of every advertised purpose and segment back to the business:
Per-purpose/segment data requirements (e.g., SMS marketing needs
buyer.phone_number) are business-specific and not enforced by UCP. Missing dependencies surface via the standard message pattern with asymmetric severity:warningon cart/checkoutcreate/update,erroroncomplete_checkout(the business MUST NOT transition tocompletedwhile consent data dependencies are unmet).{ "messages": [ { "type": "warning", "code": "missing_consent_data", "content": "Phone number is required for SMS marketing.", "path": "$.buyer.phone_number" } ] }Checklist
!for breaking changes).