Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 239 additions & 0 deletions docs/specification/loyalty.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
<!--
Copyright 2026 UCP Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

# Loyalty Extension

## Overview

The loyalty extension is designed 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 capabilities.

A few examples on how it might be used when decorating existing Capabilities:

* **Identity Linking**: For sharing membership status and point balances during account association.
* **Checkout**: For applying member-specific discounts/fulfillment benefits, calculating point earnings, or initiating reward redemptions.
* **Order**: For persisting the loyalty benefits applied to a completed transaction.

## Key Concepts

Loyalty has four main components:

**Memberships**: the overarching framework of a loyalty program, as well as the specific enrollment status and standing of a customer within it

* Usually represents a customer lifecycle solution the businesses have, which offer a tiered ecosystem of access, convenience, and identity.
* Encompasses the metadata of the concrete offerings as well as the customer level information associated with (if applicable).
* Customers can have multiple memberships that are relevant to the business (e.g. self-owned program and third-party program).

**Tiers**: progressive achievement levels within a membership that unlock increasing value based on activity or spend/fee payment

* Defines achievable levels within a membership for a customer, with various types of qualification methods and conditions.
* Customers can simultaneously be activated on and eligible for multiple tiers if the program allows.

**Benefits**: ongoing perks and privileges granted to a customer based on their current tier or membership status

* Contains both delayed (e.g. “Members have access to dedicated customer service”) and immediate-value (e.g. “Members get 5% off”) benefits.

**Rewards**: the accumulated balances and/or stored value available for the customer to redeem on transactions

* One membership can offer multiple types of accumulable/collectable rewards, each having its own usage and redemption rules.

## Discovery

Businesses can follow standard advertisement mechanism to advertise loyalty support in the Business profile

```json
{
"ucp": {
"version": "2026-01-23",
"capabilities": {
"dev.ucp.common.loyalty": [
{
"version": "2026-01-23",
"extends": "dev.ucp.shopping.checkout",
"spec": "https://ucp.dev/2026-01-23/specification/loyalty",
"schema": "https://ucp.dev/2026-01-23/schemas/common/loyalty.json"
}
]
}
}
}
```

## Schema

### Entities

#### Loyalty

{{ schema_fields('types/loyalty', 'loyalty') }}

#### Loyalty Membership

{{ schema_fields('types/loyalty_membership', 'loyalty') }}

#### Membership Tier

{{ schema_fields('types/membership_tier', 'loyalty') }}

#### Membership Tier Condition

{{ schema_fields('types/membership_tier_condition', 'loyalty') }}

#### Membership Tier Benefit

{{ schema_fields('types/membership_tier_benefit', 'loyalty') }}

#### Membership Reward

{{ schema_fields('types/membership_reward', 'loyalty') }}

#### Balance Currency

{{ schema_fields('types/balance_currency', 'loyalty') }}

#### Membership Balance

{{ schema_fields('types/membership_balance', 'loyalty') }}

#### Expiring Balance

{{ schema_fields('types/expiring_balance', 'loyalty') }}

#### Balance Redemption

{{ schema_fields('types/balance_redemption', 'loyalty') }}

### Example

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

{
"id": "membership_1",
"membership_id": "membership_id_1",
"name": "My Loyalty Program",
"enrollment_date": "2022-11-12T00:00:00Z",
"last_activity_date": "2026-01-12T00:00:00Z",
"end_date": "2036-01-12T00:00:00Z",
"tiers": [
{
"id": "tier_1",
"name": "GOLD",
"level": 1,
"benefits": [
{
"id": "BEN_001",
"title": "24-hour Early access",
"description": "24-hour early access to seasonal sales"
}
],
"conditions": [
{
"id": "CON_001",
"description": "Free to join"
}
],
"links": [
{
"type": "terms_of_service",
"url": "loyalty.com/terms",
"title": "Sign-up Terms"
}
]
},
{
"id": "tier_2",
"name": "PLATINUM",
"level": 2,
"benefits": [
{
"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.

},
{
"id": "BEN_002",
"title": "48-hour Early access",
"description": "48-hour early access to seasonal sales"
},
{
"id": "BEN_003",
"title": "Member discount",
"description": "Members get $5 off",
"applies_on": ["$.discounts.applied[0]"]
}
],
"conditions": [
{
"id": "CON_002",
"description": "$99/yr membership fee"
}
],
"links": [
{
"type": "terms_of_service",
"url": "loyalty.com/terms",
"title": "Sign-up Terms"
}
]
}
],
"activated_tiers": ["tier_1"],
"rewards": [
{
"currency": {
"name": "LoyaltyStars",
"code": "LST",
},
"balance": {
"available": 4500,
"pending": 250,
"lifetime_earned": 25000,
"lifetime_redeemed": 1000,
"expiring": [
{
"amount": 500,
"expiry_date": "2026-12-31T23:59:59Z",
"reason": "ANNUAL_EXPIRATION"
}
]
},
"redemption": {
"amount": 100,
"applies_on": ["$.discounts.applied[0]"]
}
}
]
}
],
"activated_memberships": ["membership_1"]
}
```

## Behavior and Expectations

### Request

* Non-empty `activated_memberships` and `activated_tiers` implies membership sign-up intention from the customer. No eligibility verification needed.
* Render applicable loyalty benefits based on the human-readable `title` and `description` provided, and highlight the source of immediate-value benefits alongside where they are applied based on the back referencing from `applies_on` to provide necessary disclosure or explanation.
* Only request rewards redemption when there is non-zero available balance but no need to run any extra check as redemption rules can be complicated even if there is sufficient available balance for example.

### Response

* Check and determine user’s eligibility in context (e.g. checkout with member benefits applied, checkout with rewards redemption, sign-up new membership etc) for both existing and prospective customers.
* Populate `applies_on` when immediate-value membership benefits are applicable to the transaction and reference them to the corresponding line items.
* Provide a comprehensive list of tiers that the membership offers, but at the minimum it MUST contain the ones that the customer is activated with (if applicable).
* For memberships that do not have a tiered system (i.e. loyalty level can not be upgraded or downgraded), it MUST still be treated as a single-tiered membership.
* Businesses MUST respond with at least one benefit and SHOULD aim to provide a comprehensive list.
12 changes: 12 additions & 0 deletions generate_ts_schema_types.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ async function generate() {
}
}

// Add common schemas
const commonDir = path.join(SOURCE_ROOT, 'schemas/common');
if (fs.existsSync(commonDir)) {
for (const file of fs.readdirSync(commonDir)) {
if (file.endsWith('.json')) {
properties[path.basename(file, '.json')] = {
$ref: path.join(commonDir, file)
};
}
}
}

// Add handler schemas
const handlersDir = path.join(SOURCE_ROOT, 'handlers');
if (fs.existsSync(handlersDir)) {
Expand Down
3 changes: 3 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@
# Base directories for schema resolution
OPENAPI_DIR = Path("source/services/shopping")
SHOPPING_SCHEMAS_DIR = Path("source/schemas/shopping")
COMMON_SCHEMAS_DIR = Path("source/schemas/common")
UCP_SCHEMA_PATH = Path("source/schemas/ucp.json")
SCHEMAS_DIRS = [
Path("source/handlers/google_pay"),
Path("source/schemas"),
SHOPPING_SCHEMAS_DIR,
SHOPPING_SCHEMAS_DIR / "types",
COMMON_SCHEMAS_DIR,
COMMON_SCHEMAS_DIR / "types",
]

# Cache for resolved schemas to avoid repeated subprocess calls
Expand Down
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ nav:
- Buyer Consent Extension: specification/buyer-consent.md
- Discounts Extension: specification/discount.md
- Fulfillment Extension: specification/fulfillment.md
- Loyalty Extension: specification/loyalty.md
- Cart Capability:
- Overview: specification/cart.md
- HTTP/REST Binding: specification/cart-rest.md
Expand Down Expand Up @@ -222,6 +223,7 @@ plugins:
- specification/buyer-consent.md
- specification/discount.md
- specification/fulfillment.md
- specification/loyalty.md
- specification/cart.md
- specification/cart-rest.md
- specification/cart-mcp.md
Expand Down
34 changes: 34 additions & 0 deletions source/schemas/common/loyalty.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/common/loyalty.json",
"name": "dev.ucp.common.loyalty",
"title": "Loyalty Extension",
"description": "Extends various Capabilities with loyalty support using memberships info.",
"$defs": {
"loyalty": {
"$ref": "types/loyalty.json"
},
"dev.ucp.shopping.checkout": {
"title": "Checkout with Loyalty",
"description": "Checkout extended with Loyalty capability.",
"allOf": [
{
"$ref": "../shopping/checkout.json"
},
{
"type": "object",
"properties": {
"loyalty": {
"$ref": "#/$defs/loyalty",
"ucp_request": {
"create": "optional",
"update": "optional",
"complete": "omit"
}
}
}
}
]
}
}
}
19 changes: 19 additions & 0 deletions source/schemas/common/types/balance_currency.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/common/types/balance_currency.json",
"title": "Balance Currency",
"description": "The currency of the balance.",
"type": "object",
"required": ["name", "code"],
"properties": {
"name": {
"type": "string",
"description": "Human-readable name of the currency (e.g. “LoyaltyStars”).",
"ucp_request": "omit"
},
"code": {
"type": "string",
"description": "Business-specific internal representation of the currency (e.g. “LST”)."
}
}
}
21 changes: 21 additions & 0 deletions source/schemas/common/types/balance_redemption.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/common/types/balance_redemption.json",
"title": "Balance Redemption",
"description": "The membership balance to be redeemed.",
"type": "object",
"required": ["amount"],
"properties": {
"amount": {
"type": "integer",
"minimum": 0,
"description": "The amount of available balance to be redeemed."
},
"applies_on": {
"type": "array",
"items": { "type": "string" },
"description": "JSONPaths to the discount target representing the redemption (e.g. `$.discounts.applied[0]`).",
"ucp_request": "omit"
}
}
}
24 changes: 24 additions & 0 deletions source/schemas/common/types/expiring_balance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/common/types/expiring_balance.json",
"title": "Expiring Balance",
"description": "The balance that are expiring.",
"type": "object",
"required": ["amount", "expiry_time"],
"properties": {
"amount": {
"type": "integer",
"minimum": 0,
"description": "The amount of expiring balance."
},
"expiry_time": {
"type": "string",
"format": "date-time",
"description": "Balance expiration date (RFC 3339)."
},
"reason": {
"type": "string",
"description": "Human-readable explanation of the reason for the expiring balance."
}
}
}
Loading