Discount Extension¶
Overview¶
Discount extension allows businesses to indicate that they support discount codes on cart and checkout sessions, and specifies how the discount codes are to be shared between the platform and the business.
Key features:
- Submit one or more discount codes
- Receive applied discounts with human-readable titles and amounts
- Rejected codes communicated via
messages[]with detailed error codes - Automatic discounts surfaced alongside code-based discounts
Dependencies:
- Cart Capability or Checkout Capability
Discovery¶
Businesses advertise discount support in their profile. The capability can extend cart, checkout, or both:
{
"ucp": {
"version": "2026-04-08",
"capabilities": {
"dev.ucp.shopping.discount": [
{
"version": "2026-04-08",
"extends": ["dev.ucp.shopping.cart", "dev.ucp.shopping.checkout"],
"spec": "https://ucp.dev/2026-04-08/specification/discount",
"schema": "https://ucp.dev/2026-04-08/schemas/shopping/discount.json"
}
]
}
}
}
Businesses MAY advertise discount support for cart only, checkout only, or both. Platforms SHOULD check which resources are extended before submitting discount codes.
Schema¶
When this capability is active, cart and/or checkout are extended with a
discounts object.
Discounts Object¶
Discount codes input and applied discounts output.
| Name | Type | Required | Description |
|---|---|---|---|
| codes | Array[string] | No | Discount codes to apply. Case-insensitive. Replaces previously submitted codes. Send empty array to clear. |
| applied | Array[object] | No | Discounts successfully applied (code-based and automatic). |
Applied Discount¶
A discount that was successfully applied.
| Name | Type | Required | Description |
|---|---|---|---|
| code | string | No | The discount code. Omitted for automatic discounts. |
| title | string | Yes | Human-readable discount name (e.g., 'Summer Sale 20% Off'). |
| amount | integer | Yes | Total discount amount in ISO 4217 minor units. |
| automatic | boolean | No | True if applied automatically by merchant rules (no code required). |
| method | string | No | Allocation method. 'each' = applied independently per item. 'across' = split proportionally by value. Enum: each, across |
| priority | integer | No | Stacking order for discount calculation. Lower numbers applied first (1 = first). |
| provisional | boolean | No | True if this discount requires additional verification. |
| eligibility | string | No | The eligibility claim accepted by the Business for this discount. Corresponds to a value from context.eligibility. Omitted for code-based and non-eligibility automatic discounts. |
| allocations | Array[object] | No | Breakdown of where this discount was allocated. Sum of allocation amounts equals total amount. |
Allocation¶
Breakdown of how a discount amount was allocated to a specific target.
| Name | Type | Required | Description |
|---|---|---|---|
| path | string | Yes | JSONPath to the allocation target (e.g., '$.line_items[0]', '$.totals.shipping'). |
| amount | integer | Yes | Amount allocated to this target in ISO 4217 minor units. |
Allocation Details¶
The applied array explains how discounts were calculated and distributed.
The applied[].amount describes the magnitude of the applied discount (always
positive); the corresponding totals[] entry amount represents its signed
effect on the receipt (negative for discounts).
Allocation Method¶
The method field indicates how the discount was calculated:
| Method | Meaning | Example |
|---|---|---|
each |
Applied independently per eligible item | "10% off each item" → 10% × item price |
across |
Split proportionally by value | "$10 off order" → $6 to $60 item, $4 to $40 item |
Stacking Order¶
When multiple discounts are applied, priority indicates the calculation order.
Lower numbers are applied first:
Cart: $100
Discount A (priority: 1): 20% off → $100 × 0.8 = $80
Discount B (priority: 2): $10 off → $80 - $10 = $70
The order matters because percentage discounts compound differently depending on when they're applied.
Allocations Array¶
The allocations array breaks down where each discount dollar landed, using
JSONPath to identify targets:
| Path Pattern | Target |
|---|---|
$.line_items[0] |
First line item |
$.line_items[1] |
Second line item |
$.totals.shipping |
Shipping cost |
This enables platforms to explain exactly how much each discount contributed to each line item, even when multiple discounts stack.
Invariant: Sum of allocations[].amount equals applied_discount.amount.
Operations¶
Discount codes are submitted via standard cart or checkout create/update operations. The same semantics apply to both resources.
Request behavior:
- Replacement semantics: Submitting
discounts.codesreplaces any previously submitted codes - Clear codes: Send empty array
"codes": []to remove all discount codes - Case-insensitive: Codes are matched case-insensitively by business
Response behavior:
discounts.appliedcontains all active discounts (code-based + automatic)- Rejected codes communicated via
messages[](see below) - Discount amounts reflected in
totals[]andline_items[].totals[]
Cart-to-checkout continuity: When a cart is converted to a checkout via the
cart capability's cart_id field, businesses MUST carry forward any discount
codes that were applied to the cart. Codes that are no longer valid at checkout
time (e.g., expired, ineligible) SHOULD be communicated via messages[] using
standard rejection codes.
Rejected Codes¶
When a submitted discount code cannot be applied, businesses communicate this
via the messages[] array:
{
"messages": [
{
"type": "warning",
"code": "discount_code_expired",
"path": "$.discounts.codes[0]",
"content": "Code 'SUMMER20' expired on December 1st"
}
]
}
Implementation guidance: Operations that affect order totals, or the user's expectation of the total, SHOULD use
type: "warning"to ensure they are surfaced to the user rather than silently handled by platforms. Rejected discounts are a prime example—the user expects a discount but won't receive it, so they should be informed.
Error codes for rejected discounts:
| Code | Description |
|---|---|
discount_code_expired |
Code has expired |
discount_code_invalid |
Code not found or malformed |
discount_code_already_applied |
Code is already applied |
discount_code_combination_disallowed |
Cannot combine with another active discount |
discount_code_user_not_logged_in |
Code requires authenticated user |
discount_code_user_ineligible |
User does not meet eligibility criteria |
Automatic Discounts¶
Businesses may apply discounts automatically based on cart contents, customer segment, or promotional rules:
- Appear in
discounts.appliedwithautomatic: trueand nocodefield - Applied without platform action
- Cannot be removed by the platform
- Surfaced for transparency (platform can explain to user why discount was applied)
Eligibility Claims¶
Eligibility claims are buyer claims about eligible benefits (see
Context) such as loyalty membership, payment instrument
perks, and similar. When the discount extension is active, Businesses that
choose to accept eligibility claims MUST surface their effect on pricing
as provisional discounts in the applied array. Platforms MUST display
provisional discounts to the buyer.
Discount Behavior¶
Platforms send buyer claims via context.eligibility on cart or checkout
requests (see Context). When a Business recognizes a
claim and it affects pricing, it MUST surface a corresponding provisional
discount in the discounts.applied array. This gives the Platform structured
attribution to display to the buyer.
Eligibility-triggered discounts use the following fields:
| Field | Value | Purpose |
|---|---|---|
automatic |
true |
No code required |
provisional |
true |
Requires verification at completion |
eligibility |
"com.example.store_card" |
The accepted claim |
code |
(omitted) | Not code-based |
Standard priority, method, and allocations fields apply for stacking with
other discounts.
Verification at Checkout¶
Discounts from accepted but unverified claims carry provisional: true.
Provisional discounts remain until the claim is verified, rescinded, or
replaced during the session. At checkout completion, all remaining provisional
claims MUST be resolved (see
Eligibility Verification at Completion).
Example: Provisional Discount with Attribution¶
Building on the store card example from Eligibility Verification at Completion, the discount extension provides structured attribution. The Platform claims a store card benefit; the Business surfaces the provisional discount with full stacking and allocation details:
{
"discounts": {
"applied": [
{
"title": "Store Card 5% Off",
"amount": 250,
"automatic": true,
"provisional": true,
"eligibility": "com.example.store_card",
"priority": 1,
"method": "each",
"allocations": [
{"path": "$.line_items[0]", "amount": 250}
]
}
]
},
"totals": [
{"type": "subtotal", "display_text": "Subtotal", "amount": 5000},
{"type": "items_discount", "display_text": "Discounts", "amount": -250},
{"type": "total", "display_text": "Total", "amount": 4750}
]
}
The Platform can now render: "Store Card 5% Off: -$2.50 (verified at purchase)" with full confidence in the attribution, amount, and allocation.
Impact on Line Items and Totals¶
Applied discounts are reflected in the core cart or checkout fields using two distinct total types:
| Total Type | When to Use |
|---|---|
items_discount |
Discounts allocated to line items ($.line_items[*]) |
discount |
Order-level discounts (shipping, fees, flat order amount) |
Determining the type: If a discount has allocations pointing to line
items, it contributes to items_discount. Discounts without allocations, or
with allocations to shipping/fees, contribute to discount.
| Discount Type | Where Reflected |
|---|---|
| Line-item discount | line_items[].totals[type=items_discount] |
| Order-level discount | totals[type=discount] |
Invariant: totals[type=items_discount].amount equals
sum(line_items[].totals[type=items_discount].amount).
The discounts.applied array shows what was applied. The totals[] and
line_items[].totals[] show where and how much.
Amount convention: Discount amounts in discounts.applied are positive
integers (the value of the discount). Discount entries in totals[] are
negative (the effect on the receipt) — the sign is schema-enforced.
Examples¶
Cart with discount codes¶
Discount codes applied during cart exploration. The cart response includes estimated discount amounts, giving the buyer visibility into savings before proceeding to checkout.
{
"id": "cart_abc123",
"line_items": [
{
"id": "li_1",
"item": {
"id": "prod_1",
"quantity": 2,
"title": "T-Shirt",
"price": 2000
},
"totals": [
{"type": "subtotal", "amount": 4000},
{"type": "items_discount", "amount": -800},
{"type": "total", "amount": 3200}
]
}
],
"discounts": {
"codes": ["SUMMER20"],
"applied": [
{
"code": "SUMMER20",
"title": "Summer Sale 20% Off",
"amount": 800,
"method": "each",
"allocations": [
{"path": "$.line_items[0]", "amount": 800}
]
}
]
},
"currency": "USD",
"totals": [
{"type": "subtotal", "display_text": "Subtotal", "amount": 4000},
{"type": "items_discount", "display_text": "Item Discounts", "amount": -800},
{"type": "total", "display_text": "Estimated Total", "amount": 3200}
]
}
Order-level discount¶
A flat discount applied to the order total. No allocations—the discount applies
to the order as a whole and uses type: "discount" in totals.
{
"discounts": {
"codes": ["SAVE10"],
"applied": [
{
"code": "SAVE10",
"title": "$10 Off Your Order",
"amount": 1000
}
]
},
"totals": [
{"type": "subtotal", "display_text": "Subtotal", "amount": 5000},
{"type": "discount", "display_text": "Order Discount", "amount": -1000},
{"type": "total", "display_text": "Total", "amount": 4000}
]
}
Mixed discounts (item + order level)¶
This example shows both discount types: a per-item discount (20% off) allocated to line items, and an automatic shipping discount at the order level.
{
"line_items": [
{
"id": "li_1",
"item": {
"id": "prod_1",
"quantity": 2,
"title": "T-Shirt",
"price": 2000
},
"totals": [
{"type": "subtotal", "amount": 4000},
{"type": "items_discount", "amount": -800},
{"type": "total", "amount": 3200}
]
}
],
"discounts": {
"codes": ["SUMMER20"],
"applied": [
{
"code": "SUMMER20",
"title": "Summer Sale 20% Off",
"amount": 800,
"allocations": [
{"path": "$.line_items[0]", "amount": 800}
]
},
{
"title": "Free shipping on orders over $30",
"amount": 599,
"automatic": true
}
]
},
"totals": [
{"type": "subtotal", "display_text": "Subtotal", "amount": 4000},
{"type": "items_discount", "display_text": "Item Discounts", "amount": -800},
{"type": "discount", "display_text": "Order Discounts", "amount": -599},
{"type": "fulfillment", "display_text": "Shipping", "amount": 0},
{"type": "total", "display_text": "Total", "amount": 2601}
]
}
Rejected discount code¶
When a discount code cannot be applied, the rejection is communicated via the
messages[] array. The code still appears in discounts.codes (echoed back)
but not in discounts.applied.
{
"discounts": {
"codes": ["SAVE10", "EXPIRED50"],
"applied": [
{
"code": "SAVE10",
"title": "$10 Off Your Order",
"amount": 1000
}
]
},
"totals": [
{"type": "subtotal", "display_text": "Subtotal", "amount": 5000},
{"type": "discount", "display_text": "Order Discount", "amount": -1000},
{"type": "total", "display_text": "Total", "amount": 4000}
],
"messages": [
{
"type": "warning",
"code": "discount_code_expired",
"path": "$.discounts.codes[1]",
"content": "Code 'EXPIRED50' expired on December 1st"
}
]
}
Stacked discounts with allocations¶
Multiple discounts applied with full allocation breakdown:
{
"line_items": [
{
"id": "li_1",
"item": {
"title": "T-Shirt",
"price": 6000
},
"totals": [
{"type": "subtotal", "amount": 6000},
{"type": "items_discount", "amount": -1500},
{"type": "total", "amount": 4500}
]
},
{
"id": "li_2",
"item": {
"title": "Socks",
"price": 4000
},
"totals": [
{"type": "subtotal", "amount": 4000},
{"type": "items_discount", "amount": -1000},
{"type": "total", "amount": 3000}
]
}
],
"discounts": {
"codes": ["SUMMER20", "LOYALTY5"],
"applied": [
{
"code": "SUMMER20",
"title": "Summer Sale 20% Off",
"amount": 2000,
"method": "each",
"priority": 1,
"allocations": [
{"path": "$.line_items[0]", "amount": 1200},
{"path": "$.line_items[1]", "amount": 800}
]
},
{
"code": "LOYALTY5",
"title": "$5 Loyalty Reward",
"amount": 500,
"method": "across",
"priority": 2,
"allocations": [
{"path": "$.line_items[0]", "amount": 300},
{"path": "$.line_items[1]", "amount": 200}
]
}
]
},
"totals": [
{"type": "subtotal", "display_text": "Subtotal", "amount": 10000},
{"type": "items_discount", "display_text": "Item Discounts", "amount": -2500},
{"type": "total", "display_text": "Total", "amount": 7500}
]
}
With this data, an agent can explain:
"Your T-Shirt ($60) got $12 off from the 20% summer sale, plus $3 from your