Skip to content

feat: Add basic schema for loyalty extension#251

Open
ziwuzhou-google wants to merge 5 commits intoUniversal-Commerce-Protocol:mainfrom
ziwuzhou-google:feat/loyalty
Open

feat: Add basic schema for loyalty extension#251
ziwuzhou-google wants to merge 5 commits intoUniversal-Commerce-Protocol:mainfrom
ziwuzhou-google:feat/loyalty

Conversation

@ziwuzhou-google
Copy link

@ziwuzhou-google ziwuzhou-google commented Mar 11, 2026

Summary

A new loyalty extension is introduced to facilitate high-fidelity loyalty experiences across various commerce journeys, ensuring that shoppers can for example sign-up for membership, access personalized benefits, redeem rewards, and manage memberships seamlessly across various existing and future new capabilities.

Motivation

Loyalty is a core concept in Commerce, serving as a primary driver for customer retention and long-term business growth. UCP in its current format can provide minimal and to some extent "hacky" support of loyalty in checkout for example. However, they are far from ideal and lack the generality. As Loyalty is a construct that spans across all verticals (retail, lodging, transportation, etc), the design is meant to encompass and capture the core common semantics of different capabilities and scenarios.

Design Details

Four core concepts are introduced in the schema and structured in a hierarchy:

  • Memberships: the overarching framework of a loyalty program, as well as the specific enrollment status and standing of a customer within it.
  • Tiers: progressive achievement levels within a membership that unlock increasing value based on activity or spend/fee payment.
  • Benefits: ongoing perks and privileges granted to a customer based on their current tier or membership status.
  • Rewards: the accumulated balances and/or stored value available for the customer to redeem on transactions
"memberships": [
  {
    "tiers": [
      {
        "benefits": [
          {
            ...
          }
        ]
      }
    ],
    "rewards": [
      {
        ...
      }
    ]
  }
]

With the help of these four building blocks, one can support use cases such as:

  • Checkout with loyalty benefits (e.g. discount) applied, where applies_on refers to an applied discount in the discount extension
"benefits": [
  {
    "id": "BEN_001",
    "title": "Member discount",
    "description": "Members get $5 off",
    "applies_on": ["$.discounts.applied[0]"]
  }
]
  • Request for loyalty points redemption with multiple types of rewards in one shot (real use case in Travel vertical)
"rewards": [
  {
    "currency": {
      "code": "FLYER_MILE",
    }, 
    "redemption": {
      "amount": 100
    }
  },
  {
    "currency": {
      "code": "PLUS_POINT",
    }, 
    "redemption": {
      "amount": 10
    }
  },
]
  • Customer sign-up multiple memberships (hypothetical case)
"loyalty": {
  "memberships": [
    {
      "id": "membership_1",
      "activated_tiers": ["tier_1"]
    },
    {
      "id": "membership_2",
      "activated_tiers": ["tier_2"]
    }
  ],
  "activated_memberships": ["membership_1", "membership_2"]
}

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing
    functionality to not work as expected, including removal of schema files
    or fields
    )
  • Documentation update

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

@amithanda
Copy link
Contributor

Great start on this extension!
However, Is there an issue with how request payloads are mapped when a platform attempts to redeem rewards or activate tiers?
Currently, the schema explicitly sets "ucp_request": "omit" on critical identifier fields:

  • id in source/schemas/common/types/loyalty_membership.json
  • currency in source/schemas/common/types/membership_reward.json

The Problem:
If a customer has multiple different memberships (e.g., standard vs credit cardholder) or a single membership with multiple reward currencies (e.g., Points vs Miles), a platform needs a way to explicitly address exactly which membership or reward balance they are redeeming during a Checkout update or Checkout complete request.
Because id and currency are omit, the client is technically forbidden from sending them in the update payload. If a client sends this:

"loyalty": {
  "memberships": [
    {
      "activated_tiers": ["T_1"],
      "rewards": [ 
        { "redemption": { "amount": 100 } } 
      ]
    }
  ]
}

The business server has no stable identifier to know which membership ID T_1 belongs to, or which currency 100 refers to, except by relying on brittle array indexing which changes between requests.

@ziwuzhou-google
Copy link
Author

Great start on this extension! However, Is there an issue with how request payloads are mapped when a platform attempts to redeem rewards or activate tiers? Currently, the schema explicitly sets "ucp_request": "omit" on critical identifier fields:

  • id in source/schemas/common/types/loyalty_membership.json
  • currency in source/schemas/common/types/membership_reward.json

The Problem: If a customer has multiple different memberships (e.g., standard vs credit cardholder) or a single membership with multiple reward currencies (e.g., Points vs Miles), a platform needs a way to explicitly address exactly which membership or reward balance they are redeeming during a Checkout update or Checkout complete request. Because id and currency are omit, the client is technically forbidden from sending them in the update payload. If a client sends this:

"loyalty": {
  "memberships": [
    {
      "activated_tiers": ["T_1"],
      "rewards": [ 
        { "redemption": { "amount": 100 } } 
      ]
    }
  ]
}

The business server has no stable identifier to know which membership ID T_1 belongs to, or which currency 100 refers to, except by relying on brittle array indexing which changes between requests.

Both are correct. These identifiers should not have omit. See the updated example in #251 (comment) for how platform can request redemption with multiple types of award & sign-up for multiple memberships.

Copy link

@sinhanurag sinhanurag left a comment

Choose a reason for hiding this comment

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

Good start based on all our discussions! Left some comments. Let's iterate on it more and discuss further.

{
"id": "BEN_001",
"title": "Free shipping",
"description": "Complimentary standard shipping on all orders"

Choose a reason for hiding this comment

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

We will need some examples of cart, checkout, order objects (starting with checkout is fine) which have this extension as well as the discount object clearly displaying the benefit being applied and the discount object enumerating the benefit.

Like in this case "BEN_001" will apply to discounts.applied.allocations (path = fulfillment-options.total , amount = xx) etc.

Ref: https://ucp.dev/latest/specification/discount/#discounts-object

Copy link
Author

Choose a reason for hiding this comment

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

Added an example here for shipping but I don't think the applies_on needs to go all the way to allocations level as the shipping benefit applies on the entire order.


```json
{
"memberships": [

Choose a reason for hiding this comment

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

Can we also add a request/response example of how an instrument based discount will look like?

I am thinking of cases when there is a co branded instrument used for purchase. At that point in time the agent might or might not be aware of the associated discount. The agent will send the chosen FOP or identifier as a signal. This signal can be part of the loyalty extension in the incoming checkout/cart request or passed in a different placeholder. The response should contain a loyalty extension with the applied discount based on the instrument.

Choose a reason for hiding this comment

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

I am actually fine reconciling it with the PR raised here - #250

-cc @igrigorik

So the ingress signal for loyalty identification can come from context.eligibility but then the response can contain the loyalty extension decorating the objects.

Copy link
Contributor

Choose a reason for hiding this comment

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

A thought on this point:

So the ingress signal for loyalty identification can come from context.eligibility but then the response can contain the loyalty extension decorating the objects.

Given context.eligibility uses reverse domain names, I think one simple way to reconcile the 2 approaches would be to either have memberships[].id to also be reverse domain names OR add another field to reference back to the corresponding eligibility (similar to the provisional discount).

What I feel also fits well between the 2 PR proposals is that in the case any generic (not necessarily instrument-based discount) loyalty eligibility verification fails (i.e. buyer is not actually properly signed up to the loyalty program), this extension offers a mechanism for platform to request for sign-ups and renegotiate for that verification.

Copy link
Contributor

Choose a reason for hiding this comment

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

add another field to reference back to the corresponding eligibility (similar to the provisional discount).

This is what I was expecting, for symmetry with discounts. From #250, both provisional and eligibility from the applied discount type feel appropriate to include on each membership, too.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would love to see an input example for specifying an reward balance redemption, though — it seems like that is brand new input as part of this capability.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the discussion here. I agree that we should add both provisional and eligibility, to handle monetary benefits (e.g. member pricing, points redemption) that links to a discount object (more specifically, discounts.applied field), AND non-monetary benefits (e.g. points/cashback earning based on transaction amount - 20 pts per dollar spent, 5 miles per dollar in base fare etc), as they all need verification from business side to be honored.

Now the question is where to, at membership level, tier level, or even benefit level? Given eligibility claims for loyalty are verified at a tier level (not membership level as businesses not only need to check if buyer is a member of the program but also which specific tier buyer is in to give corresponding benefits; also not benefit level as normally eligible for a tier means all benefits under that tier are available for the buyer), I personally felt both provisional and eligibility should be added under tier.

Concretely, a request/response would look like this for point redemption:

=== "Request"

    {
      "context": {
        "eligibility": ["com.example.loyalty_tier_1"]
      },
      "line_items": [
        {
          "item": {
            "id": "prod_shirt",
            "quantity": 2,
            "price": 2500
          }
        }
      ],
      "loyalty": {
        "memberships": [
          {
            "id": "membership_1",
            "member_id": "member_id_1",
            "rewards": [
              {
                "currency": {
                  "code": "LST"
                },
                "redemption": {
                  "amount": 100
                }
              }
            ]
          }
        ]
      }
    }

=== "Response"

    {
      "discounts": {
        "applied": [
          {
            "title": "Point Redemption",
            "amount": 250,
            "automatic": true,
            "provisional": true,
            "eligibility": "com.example.loyalty_tier_1",
            "priority": 1,
            "method": "each",
            "allocations": [
              {"path": "$.line_items[0]", "amount": 250}
            ]
          }
        ]
      },
      "loyalty": {
        "memberships": [
          {
            "id": "membership_1",
            "member_id": "member_id_1",
            "name": "My Loyalty Program",
            "tiers": [
              {
                "id": "tier_1",
                "name": "GOLD",
                "level": 1,
                "provisional": true,
                "eligibility": "com.example.loyalty_tier_1",
              }
            ],
            "activated_tiers": ["tier_1"],
            "rewards": [
              {
                "currency": {
                  "name": "LoyaltyStars",
                  "code": "LST"
                },
                "balance": {
                  "available": 4500
                },
                "redemption": {
                  "amount": 100,
                  "applies_on": "$.discounts.applied[0]"
                }
              }
            ]
          }
        ],
        "activated_memberships": ["membership_1"]
      },
      "totals": [
        {"type": "subtotal", "display_text": "Subtotal", "amount": 5000},
        {"type": "items_discount", "display_text": "Point Redemption", "amount": -250},
        {"type": "total", "display_text": "Total", "amount": 4750}
      ]
    }

@lemonmade @jingyli @sinhanurag does this make sense?

@ziwuzhou-google ziwuzhou-google marked this pull request as ready for review March 12, 2026 02:24
@ziwuzhou-google ziwuzhou-google requested review from a team as code owners March 12, 2026 02:24
Copy link

@tpindel tpindel left a comment

Choose a reason for hiding this comment

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

Great work on this extension @ziwuzhou-google 👏 the hierarchical memberships → tiers → benefits → rewards model is well-designed, and placing it in common/ for cross-vertical use is the right call.

We've been building a reference implementation of incentives + loyalty on UCP (MCP transport, Shopify + Voucherify API/MCP, ~22 ucp_* tools covering the full shopping lifecycle). Based on real agent-customer interactions, there are a few capabilities we've found essential that the current schema doesn't cover yet. Happy to contribute PRs for any of these if there's interest.

1. Earning Forecast - "What will I earn from this purchase?"

This is the single most impactful missing piece for agent-driven commerce. In our implementation, showing "You'll earn 120 Loyalty Stars/Points with this order, bringing your balance to 4,620" before the customer commits is one of the strongest purchase motivators. It also helps agents handle price objections - points earning becomes additional value on top of any discount.

The current schema has balance (what you have) and redemption (what you're spending), but no way to show what the customer gains from the transaction.

Proposed addition - a new earning_forecast field on membership_reward, response-only:

"earning_forecast": {
    "type": "object",
    "description": "Preview of rewards to be earned from the current transaction.",
    "ucp_request": "omit",
    "properties": {
      "amount": {
        "type": "integer",
        "minimum": 0,
        "description": "Total points to be earned if the transaction completes."
      },
      "rules": {
        "type": "array",
        "items": {
          "type": "object",
          "required": ["id", "amount"],
          "properties": {
            "id": { "type": "string", "description": "Earning rule identifier." },
            "description": { "type": "string", "description": "Human-readable explanation (e.g. '2x points on footwear')." },
            "amount": { "type": "integer", "minimum": 0, "description": "Points earned from this rule." }
          }
        },
        "description": "Breakdown of earning rules contributing to the total."
      },
      "projected_balance": {
        "type": "integer",
        "minimum": 0,
        "description": "Projected available balance after earning and any redemption from this transaction."
      }
    }
}

This fits naturally into the existing membership_reward structure - each reward currency gets its own earning forecast. The per-rule breakdown gives agents transparency to explain why the customer is earning (e.g. "2x points on footwear" vs "base earning on all purchases"), which makes loyalty feel tangible rather than a black box.

2. Tier Progression - "How close am I to the next tier?"

The current schema shows which tiers exist and which are activated, but doesn't tell the agent how close the customer is to qualifying for a higher tier. In practice this drives natural upsell: "You're only 200 points from Platinum - this purchase would get you there!"

Proposed addition to membership_tier:

"progression": {
    "type": "object",
    "description": "Customer's progress toward qualifying for this tier.",
    "ucp_request": "omit",
    "properties": {
      "qualified": {
        "type": "boolean",
        "description": "Whether the customer currently qualifies for this tier."
      },
      "progress_amount": {
        "type": "integer", "minimum": 0,
        "description": "Current progress value toward qualification threshold."
      },
      "threshold_amount": {
        "type": "integer", "minimum": 0,
        "description": "Required value to qualify for this tier."
      },
      "remaining_amount": {
        "type": "integer", "minimum": 0,
        "description": "Gap between current progress and threshold."
      }
    }
}

This is also response-only. For tiers the customer already has, qualified: true. For higher tiers, the agent can use remaining_amount to suggest cart additions that would push the customer over the threshold.

3. Agent Journey Example - When to Present Loyalty Context

Regarding PR #250 (eligibility claims) - in our implementation the agent flow looks like:

  1. Identify - customer provides email or loyalty card → agent looks up member profile (memberships, tiers, rewards balance)
  2. Discover deals - agent checks what promotions + loyalty benefits apply to the current cart, including tier-specific benefits
  3. Present - agent shows price with discount + "you'll earn X points" + "you're Y away from next tier"
  4. Redeem (optional) - customer chooses to spend points → redemption.amount on the relevant reward, linked via applies_on to discount extension
  5. Complete - earning is confirmed in the response

A concrete request/response example in the spec doc showing this full flow - especially the interplay between loyalty benefits, discount applies_on references, beast deals, and earning in the response - would be very helpful for agent developers building on this extension.

Context
We have a working MCP-based UCP server implementing these patterns - earning forecast and the full incentives negotiation lifecycle (search best deal → validate → lock → redeem). Happy to share more details or contribute schema additions as a follow-up PR if any of these proposals are useful.

@ziwuzhou-google
Copy link
Author

@tpindel Thanks a lot for the comprehensive and well-thought comment. Really appreciate it.

First-off, points earning is actually well planned in my head. The main reason I dropped it is just I want to have a simple start with the basics (e.g. the hierarchical data model) aligned with the community, and focus on those immediate-value monetary benefits (e.g. member pricing and shipping). But totally agree that points is a big motivator, especially in certain vertical (e.g. Hotel, Flights) where point is probably the single most important benefit.

Now coming back to your individual proposal:

1 - Earning Forecast
I think the proposal looks directionally correct to me but I have two additional thoughts:

  • It lacks the capability to have a per-item break-down. The amount gives the transaction level total and rules shows the break-down at that level as well. However, the agent cannot know per-item earnings (and associated breakdown). As a comparison, when there is a pricing related loyalty discount, we have this applies_on field referring to the applied discount in the discount extension, and the allocations in the applied field there provides the per-item breakdown. Maybe we should have similar allocations here to reference back the items?
  • The data modeling between monetary benefits and these reward point earnings are scattered into two object benefit (nested under tier) and reward (standalone), although in business' mindset they are all just perks. I'm not sure if this creates a sense of mental burden in implementation. In the original PR, rewards is used to just hold tier-agnostic objects (balances and redemption) and to me these point earnings are tied to the tier customer is in (higher tier usually gives better rates). I do acknowledge the proposal fits very naturally into the structure of rewards with the currency there, but I'm just thinking if we should aim for a better grouping of the "gains" (i.e. the discount, the point earning) that are tier specific, or this is an over-optimization?

2 - Tier Progression
Agree that the progression object should go into the tier but I also see a few things worth discussion:

  • Right now the progression is hinged on a single amount value, but I do know cases where achieving next level requires meeting threshold for multiple conditions (e.g. 100 Loyalty Stars AND 5 Loyalty Moons). Extending to support this withe proposal looks like a breaking change to me? Also what if there are conditions that are irrelevant to these accruable points (e.g. you need to have at least 10 transactions)?
  • The threshold is somewhat duplicated with the membership_tier_condition? Ack that right now it is holding pure text strings but I think we can extend that to hold more quantifiable values.

And yes we will add concrete request/response examples for that flow to show how these extensions interact with each other!

Lastly, there is no doubt that follow-up PRs from your side to enhance the basic schema are extremely welcomed. These use cases are solid ones that I believe lots of businesses share to have or desire to start with.

Thanks again for the nice proposal!

@tpindel
Copy link

tpindel commented Mar 13, 2026

@ziwuzhou-google Thanks for comment and respond to me. I will try to address your questions.

1 - Earning Forecast
I think the proposal looks directionally correct to me but I have two additional thoughts:

It lacks the capability to have a per-item break-down. The amount gives the transaction level total and rules shows the break-down at that level as well. However, the agent cannot know per-item earnings (and associated breakdown). As a comparison, when there is a pricing related loyalty discount, we have this applies_on field referring to the applied discount in the discount extension, and the allocations in the applied field there provides the per-item breakdown. Maybe we should have similar allocations here to reference back the items?

I think we should remain open in the standard for that. From my experience at Voucherify, the majority of clients start with a more simplified approach where the points forecast is for the entire transaction or order. It is rare for them to switch to a per-item split later on.

Even though people often design programs with a detailed breakdown in mind, I’ve seen many cases where, after launch, they never actually return to implement it. From my point of view, the standard should define the breakdown, but it should also allow for a simpler option. If a program isn't able to provide a per-item breakdown, the agent should be able to simply use the total forecasted amount. Does that make sense?

The data modeling between monetary benefits and these reward point earnings are scattered into two object benefit (nested under tier) and reward (standalone), although in business' mindset they are all just perks. I'm not sure if this creates a sense of mental burden in implementation. In the original PR, rewards is used to just hold tier-agnostic objects (balances and redemption) and to me these point earnings are tied to the tier customer is in (higher tier usually gives better rates). I do acknowledge the proposal fits very naturally into the structure of rewards with the currency there, but I'm just thinking if we should aim for a better grouping of the "gains" (i.e. the discount, the point earning) that are tier specific, or this is an over-optimization?

From my point of view, it is better if rewards and perks are listed generally and can be assigned to tiers from a wider pool based on particular conditions. This should apply to earning rules as well. For instance, in higher tiers, you might have better earning conditions, or you might pay less (in loyalty currency) to receive a perk. In general, I would say that bonuses and perks should be globally configurable, with adjusted variants then linked to specific tiers.

Regarding 'rewards,' I would prefer to use that term for defining specific perks or incentives - like coupons, material, rewards, extra points, gift, or gift cards - that you receive for achieving a milestone or by spending points (exchange points <-> reward). I'm not convinced that this name should be linked only to accumulated balances. In loyalty programs, the different buckets used to collect various currencies (even within the same program) are typically called Points Wallets (I also know name sub-ledgers). These represent different types of point rewards or sub-wallets used to collect specific currencies for particular groups of earning rules.

2 - Tier Progression
Agree that the progression object should go into the tier but I also see a few things worth discussion:
Right now the progression is hinged on a single amount value, but I do know cases where achieving next level requires meeting threshold for multiple conditions (e.g. 100 Loyalty Stars AND 5 Loyalty Moons). Extending to support this withe proposal looks like a breaking change to me? Also what if there are conditions that are irrelevant to these accruable points (e.g. you need to have at least 10 transactions)?

From my POV, in practice, the base use case is definitely a member progressing through tiers based on accumulated points. The typical scenario involves the current balance or points accumulated within defined timeframes. Tiers defined by point accumulation across different wallets are rarer cases. I think typical case is points wallet and linked to it tiers structure.

Sometimes, what I’ve seen is that progression is based on additional metadata attributes describing the customer or other segment definitions (e.g., location, total order count in the last 12 months, sign-up date, etc.). And yes, you are right - I wouldn't make it more complex at this stage by adding things like the number of transactions. That part can actually be hidden in the loyalty engine layer as a specific additional attribute, perhaps explained in the text-based terms and conditions or something similar.

The threshold is somewhat duplicated with the membership_tier_condition? Ack that right now it is holding pure text strings but I think we can extend that to hold more quantifiable values.

Indeed

Copy link
Contributor

@lemonmade lemonmade left a comment

Choose a reason for hiding this comment

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

Learned a lot about the domain, and this generally hung together for me as someone unfamiliar with the ins and outs of loyalty.

"id": "BEN_001",
"title": "Free shipping",
"description": "Complimentary standard shipping on all orders",
"applies_on": ["$.discounts.applied[0]"]
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like a lot of the display information for the referenced applied discount information is likely to be duplicated on this object. Is there any way to deduplicate these two (e.g., by splitting applied_benefits: ["discounts.keypath"] from available_benefits), or do we need the full-fidelity information on the membership itself for the host to render this on the business's behalf?

Copy link
Contributor

Choose a reason for hiding this comment

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

If we leave the applies_on here, when could it point to more than one resource in checkout?

Copy link
Author

@ziwuzhou-google ziwuzhou-google Mar 19, 2026

Choose a reason for hiding this comment

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

1). I also share the same feeling about duplicated message (i.e. title here v.s. title within the referenced discounts.applied field). My thinking is we can put some guidance in our spec documentation that asks platform MUST use one and only one of it in this case (and I prefer the one referenced applied discount). Splitting that referencing achieved by applies_on out of benefit makes it hard to understand the linkage between the referenced object and benefit within membership. In the meantime, I do think having the full-fidelity information on the membership side will help platform as they can choose to display the corresponding tier name for clarity for example. I just don't see enough damage/confusion caused by this nesting that worth the effort to move it out and lose the "connection".

2). That is a very good point about about applies_on being an array. It does not need to be actually. If there is a benefit that touches more than one resource in checkout. it should always be possible to split the benefit into multiple non-overlapping ones. Updated!

"type": "string",
"description": "Short and succinct explanation of the tier participation condition (e.g. “Free to join”)."
},
"condition_type": {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does the platform need to know this? I feel like this list is probably not comprehensive, but will require breaking changes to add to in the future. What will a host do with this information, beyond just printing description?

Copy link
Author

Choose a reason for hiding this comment

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

As you pointed out, the purpose is really for displaying the condition. In the flow of membership sign-up, platform needs to at least display some short excerpt of sign up requirement (both due to legal requirement and as an upsell), and provide a full link of terms & conditions to fully explain the nitty-gritty in case user wants to learn more.

Copy link
Contributor

Choose a reason for hiding this comment

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

There are some parts of this document that read a little more mechanical than other specification docs in UCP. I’d recommend putting this new spec and its siblings, like fulfillment, discount, and checkout, through your favorite LLM. I would at least expect to see a bit more guidance using the MUST/ SHOULD/ MAY structure of the other specifications.

}
}
}
```
Copy link
Contributor

Choose a reason for hiding this comment

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

There's no mention/ link to identity linking or context.eligibility in here; should there be? I assume most of the application logic in here will hinge on providing one of those two signals to checkout.

"activated_memberships": {
"type": "array",
"items": { "type": "string" },
"description": "List of memberships that the customer is activated with, identified by the `id` of the Loyalty Membership object."
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be ucp_request: omit?

Copy link
Author

@ziwuzhou-google ziwuzhou-google Mar 18, 2026

Choose a reason for hiding this comment

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

Request can set this field to request for membership sign up like below (multiple memberships in hypothetical case):

"loyalty": {
  "memberships": [
    {
      "id": "membership_1",
      "activated_tiers": ["tier_1"]
    },
    {
      "id": "membership_2",
      "activated_tiers": ["tier_2"]
    }
  ],
  "activated_memberships": ["membership_1", "membership_2"]
}

Maybe the argument is: with activated_tiers + id set in the request, do we still need to set activated_memberships (as it seems redundant with the id field). My thought is yes / no. Having it available in the request can help the business to cross-check the platform is indeed requesting for "membership_1", "membership_2", and this field is optional in essence (correct me if I'm wrong here as we don't have it in the required field for the entire loyalty object).

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think I understood that you intended to have this be mutable at all. To me, it feels odd that the platform would activate memberships on the business by way of a update_checkout mutation. I don’t think there are other mutations you can perform on checkout today that would have a side effectin this same way. Is this really an operation we expect to perform mid-checkout, without any additional confirmation/ payment/ follow-up beyond what can fit in a successful checkout update response?

Copy link
Author

Choose a reason for hiding this comment

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

I might miss some of the setup/syntax in schema definition but this is never intended to be bind with checkout (yes loyalty extension can decorate checkout but not a must). My thought was the loyalty sign-up happened in the upper funnel discovery phase: 1). platform advertise the business loyalty program, potential benefits and there are also info provided about the sign-up requirement; 2). then platform can ask if user want to sign up a particular tier; 3). user said yes with platform sending above mentioned request to business.

Later in the real checkout flow after use has signed up, platform just send a request like this and it's up to business to verify that eligibility claim:

    {
      "context": {
        "eligibility": ["com.example.loyalty_tier_1"]
      },
      "line_items": [
        {
          "item": {
            "id": "prod_shirt",
            "quantity": 2,
            "price": 2000
          }
        }
      ]
    }

Copy link
Contributor

Choose a reason for hiding this comment

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

OK, I was also expecting that sign up for layalty would happen pre-checkout, so it seems odd that sign up would ever happen through Checkout operations, by specifying the above input. Shouldn’t there be dedicated operations for loyalty membership sign up/ updates?

Copy link
Author

Choose a reason for hiding this comment

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

Correct there should be one. I think there are lots of open discussion about loyalty membership sign up/ updates around how we should model it. One possibility is we have a high level account management capability (beyond membership specific updates) and loyalty extension is used to decorate it; the other is Loyalty/Membership management has its own capability for all the operations. I don't think this part has been fully nailed down (and it is not the plan of this PR).

"activated_tiers": {
"type": "array",
"items": { "type": "string" },
"description": "List of tiers that the customer is activated for or wish to activate, identified by the `id` of the Membership Tier object."
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be ucp_request: omit?

Copy link
Author

Choose a reason for hiding this comment

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

See one of the above comments. This should not as it will allow the platform to request for membership sign-up, and in the request it needs to specify which tier they are trying to sign up for.


```json
{
"memberships": [
Copy link
Contributor

Choose a reason for hiding this comment

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

I would love to see an input example for specifying an reward balance redemption, though — it seems like that is brand new input as part of this capability.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants