Skip to content

feat!: buyer consent with per-segment extensibility#451

Draft
igrigorik wants to merge 2 commits into
mainfrom
feat/consent_segments
Draft

feat!: buyer consent with per-segment extensibility#451
igrigorik wants to merge 2 commits into
mainfrom
feat/consent_segments

Conversation

@igrigorik
Copy link
Copy Markdown
Contributor

Evolves the existing buyer_consent extension 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:

...
"buyer": {
  "consent": {
    "analytics": true,
    "sale_of_data": false,
    "marketing": {
      "dev.ucp.consent.email": {
        "description": "Promotional emails and exclusive offers",
        "links": [{ "type": "privacy_policy", "url": "https://example.com/privacy" }],
        "allowed": false
      },
      "dev.ucp.consent.sms": {
        "description": "Order updates via SMS",
        "links": [{ "type": "tcpa_disclosure", "url": "https://example.com/sms-terms" }],
        "allowed": false
      }
    }
  }
}

Existing marketing: true | false payloads 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:

Direction Who populates Fields populated
Advertise (response) Business description, links, current allowed
Confirm (request) Platform captured allowed

Business advertises in create_checkout / update_checkout response with the buyer's prior decision or the business's default for each segment. Platform captures the buyer's decisions and updates via:

...
"buyer": {
  "consent": {
    "marketing": {
      "dev.ucp.consent.email": { "allowed": true },
      "dev.ucp.consent.sms":   { "allowed": false },
      "com.chatapp.something": { "allowed": false}
    }
  }
}

Per-segment data requirements (e.g., dev.ucp.consent.sms needs buyer.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

  • Capability: New schemas (Discovery, Cart, etc.) or extensions.
  • I have followed the Contributing Guide (including Conventional Commits title requirements and ! for breaking changes).
  • I have updated the documentation (if applicable).
  • My changes pass all local linting and formatting checks.

   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
@igrigorik igrigorik added the TC review Ready for TC review label May 19, 2026
Copy link
Copy Markdown
Contributor

@jamesandersen jamesandersen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"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."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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": {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this will be a breaking change anyway... do we have some conviction that:

  1. This alternate bool modeling is useful to keep around?
  2. We've got the right four categories e.g. analytics, marketing, preferences, sale_of_data? e.g. if introducing a more richly modeled consent_segment are 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 for ad_storage, ad_user_data and analytics_data ... are those all analytics (because it's GTM) or are some marketing (ads)?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...

  1. 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 create makes sense? ... maybe if cart was used first and it's assumed the consent segments are the same? Is that safe?
  2. renders checkout UX with a buyer's primary contact, shipping and payment information and applicable consent UX from the create response
  3. user reviews checkout details and interacts with the consent UX as applicable before confirming checkout
  4. Platform now calls complete checkout 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?

Copy link
Copy Markdown

@wsbrunson wsbrunson May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

@amithanda
Copy link
Copy Markdown
Contributor

Hi @igrigorik, @jamesandersen

Thanks for putting this together. Evolving buyer_consent to support granular, per-segment consent is a very clean way to address regulatory requirements without locking the schema into a fixed set of channels.

I’d like to highlight 3 core architectural topics for us to discuss and align on, building upon some of @jamesandersen's points:

1. Polymorphism (Boolean vs. Segment Map)

As @jamesandersen pointed out, since this is already a breaking change, we should evaluate if keeping the hybrid boolean | object modeling is the right path.

  • Implications: Allowing a property to accept either a primitive boolean or a complex object introduces type polymorphism. This complicates runtime parsing and often generates messy/ambiguous union types in standard client-side model-generation tools.
  • Discussion Point: If we want to support granular consent, should we completely deprecate the raw boolean values and standardise on a single object-based configuration? Furthermore, should we dispense with the intermediate strict category hierarchy (marketing, analytics, etc.) and instead let businesses express a flat map/array of consent segments directly under buyer.consent?

2. Consent Flow and Lifecycle (update vs. complete & PII)

I want to echo @jamesandersen’s recommendation to allow the buyer object on checkout complete (making "complete": "optional").

  • The Latency Problem: Surfacing shipping, contact, and consent forms on the final screen is a standard checkout layout. If the buyer object is omitted on complete, the platform is forced to run a blocking update_checkout API call to save consent states right before triggering the final payment. This blocking round-trip at the conversion point adds unnecessary latency.
  • The PII Disclosure Boundary: For privacy-centric platforms (e.g., assistants/wallets that withhold buyer contact PII until the user actively confirms the order), submitting buyer.phone_number or buyer.email is reserved for the final complete call. If consent dependencies (like SMS requiring a phone number) are validated during update, these privacy flows are broken. Allowing buyer and consent states to be submitted atomically on complete resolves both latency and PII boundary conflicts.

3. Default Consent Values & Jurisdictional Gaps

Building on @jamesandersen’s note that "some jurisdictions should be modeled as opt-in, some as opt-out and it's the business' responsibility to own that decision":

  • The Compliance Gap: If a US-based merchant defaults a checkbox to checked ("allowed": true), but the platform renders the checkout for a user in the EU (where pre-checked marketing boxes are illegal under GDPR), the platform must override the merchant default and show the box unchecked.
  • Lack of Intent Signaling: The current schema only communicates the final allowed state. The merchant has no way of knowing if a false value represents a user actively opting out (unchecking a box) vs. a platform default override (GDPR opt-in rendering where the user just ignored the box).
  • Discussion Point: Should we:
    1. Add explicit metadata to the segment payload (e.g., consent_model: "opt_in" | "opt_out") so the platform knows how to legally render the defaults?
    2. Introduce a signal like explicit_choice: boolean or user_interacted: boolean on confirm requests to help merchants audit and verify active user consent decisions?

   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.
@igrigorik
Copy link
Copy Markdown
Contributor Author

@jamesandersen, @amithanda, @wsbrunson ty for the feedback!

Updated to allow consent on complete (dfc5619).

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...

Consent > Purpose > Segment is load-bearing structure.

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 description of each object and there is no easy or logical grouping unless agent leans on substring matches (not guaranteed to work reliably) or spends LLM tokens to group and present a coherent choice matrix: as a user I may want to opt out from all marketing, or only consent to a particular segment. This places the burden on the agent to present and get the choice matrix right, which is an antipattern that lands everyone (agent, business, user) in non-deterministic (bad) outcome.

Channels / segments at top level are footguns

As an example, dev.ucp.consent.email at top level carries a footgun. If buyer says no, does that mean that order confirmation and update emails are blocked? With segmen keys at the top, description becomes load-bearing to capture purpose. This is fragile and adds another complication on top of above concerns.

(Recommended) nested shape: Purpose > (optional) Segments

We can normatively spec that businesses must provide purpose > segment structure...

"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:

  • dev.ucp.consent.marketing
  • dev.ucp.consent.analytics
  • dev.ucp.consent.preferences
  • dev.ucp.consent.sale_or_sharing (renamed from sale_of_data)

Businesses are free to define own purpose buckets using rDNS convention, under which they slot optional segments. The top level contract of buyer.consent > purpose.description > [segment.description] allows agent to faithfully render the choice matrix without any guessing or heuristics.

@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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

TC review Ready for TC review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants