This guide explains how to apply, validate, and display promotional codes in the Universal Commerce Protocol (UCP) checkout flow.
When a user initiates a checkout session, Google provides promotional data from multiple sources. Your integration must validate these promotions in real time and return a granular line-item breakdown of the discounts.
Discovery
To receive discount codes from Google, you must advertise discount support in your profile.
Version 2026-04-08
In version 2026-04-08, platforms check if the checkout capability is extended before submitting discount codes.
{ "ucp": { "version": "2026-04-08", "capabilities": { "dev.ucp.shopping.discount": [ { "version": "2026-04-08", "extends": ["dev.ucp.shopping.checkout"], "spec": "https://ucp.dev/2026-04-08/specification/discount", "schema": "https://ucp.dev/2026-04-08/schemas/shopping/discount.json" } ] } } }
Version 2026-01-23
In version 2026-01-23, the discount capability only extends checkout sessions.
{ "ucp": { "version": "2026-01-23", "capabilities": { "dev.ucp.shopping.discount": [ { "version": "2026-01-23", "extends": "dev.ucp.shopping.checkout", "spec": "https://ucp.dev/2026-01-23/specification/discount", "schema": "https://ucp.dev/2026-01-23/schemas/shopping/discount.json" } ] } } }
Promotion sources
Google provides promotional data from several sources. Discounts are either automatically applied to the cart or manually entered by the user.
Auto-applied promotion codes
- Google Merchant Center promotions: Offers ingested directly from your Google Merchant Center feeds.
User-applied promotions
- User-entered codes: Promotional codes users manually enter during checkout, such as public discount codes or personalized email offers. The Google checkout UI supports a maximum of 10 applied promo codes per session.
Checkout API implementation
When checkout session is created, or when a user modifies a promotional code
during checkout, Google sends a request to your POST /checkout-sessions or
PUT /checkout-sessions/{id} endpoints.
- Requested codes (
discounts.codes): Google submits the user's promo codes in this array. You must echo this array back in your response to maintain state. The response array index (for example,$.discounts.codes[1]) is used to map validation warnings back to specific codes.- Replacement semantics: Submitting this array replaces any previously submitted codes.
- Clear codes: Sending an empty array
[]removes all discount codes. - Case-insensitive: Codes must be matched case-insensitively by your business logic.
- Applied discounts (
discounts.applied): If a code is valid, include it in this array along with a human-readabletitle, the positiveamountof the discount, and the calculationmethod. - Rejections (
messages): If a code is invalid, omit it from theappliedarray and communicate the failure reason using a canonical warning in themessagesarray.
Impact on line items and totals
Applied discounts are reflected in the core checkout fields using two distinct
total types. If a discount has allocations pointing to line items, it
contributes to items_discount. Discounts without allocations, or with
allocations to shipping or fees, contribute to discount.
| Discount type | Total type | Where reflected |
|---|---|---|
| Line-item discount | items_discount |
line_items[].totals[type=items_discount] |
| Order-level discount | discount |
totals[type=discount] |
Version differences:
How you format the discount amount in the totals array depends on your API
version:
- Version
>= 2026-04-08: Discount entries intotals[]andline_items[].totals[]must use negative values to reflect their subtractive effect on the receipt. - Version
2026-01-23: Discount amounts intotals[]andline_items[].totals[]are represented as positive integers.
Mathematical invariants: To ensure data integrity and proper receipt rendering, your integration must adhere to the following mathematical rules:
- Allocation sum: The sum of
allocations[].amountmust equal theapplied_discount.amount. - Line-item totals sum:
totals[type=items_discount].amountmust equal the sum ofline_items[].totals[type=items_discount].amount.
API examples for auto-applied promotions
The following examples demonstrate discounts applied automatically. The request
payloads don't contain a discounts.codes array, but the responses include
"automatic": true and omit the code field.
Version 2026-04-08
In version 2026-04-08, discount entries in totals[] and line_items[].totals[] use negative values.
Auto-applied item-level discount
A 10% sitewide sale automatically applied to a specific line item.
// Request Example { "line_items": [ { "id": "li_1", "item": { "title": "Sneakers", "price": 10000 }, "quantity": 1 } ] }
// Response Example { "line_items": [ { "id": "li_1", "item": { "title": "Sneakers", "price": 10000 }, "quantity": 1, "totals": [ {"type": "subtotal", "amount": 10000}, {"type": "items_discount", "amount": -1000}, {"type": "total", "amount": 9000} ] } ], "discounts": { "applied": [ { "title": "10% Off Sitewide Sale", "amount": 1000, "automatic": true, "method": "each", "allocations": [ {"path": "$.line_items[0]", "amount": 1000} ] } ] }, "totals": [ {"type": "subtotal", "display_text": "Subtotal", "amount": 10000}, {"type": "items_discount", "display_text": "Item Discounts", "amount": -1000}, {"type": "total", "display_text": "Total", "amount": 9000} ] }
Auto-applied order-level discount
A promotional rule (e.g., "$10 off orders over $50") applied to the order as a whole without specific line-item allocations.
// Request Example { "line_items": [ ... ] }
// Response Example { "line_items": [ ... ], "discounts": { "applied": [ { "title": "$10 Off Orders Over $50", "amount": 1000, "automatic": true } ] }, "totals": [ {"type": "subtotal", "display_text": "Subtotal", "amount": 6000}, {"type": "discount", "display_text": "Order Promo", "amount": -1000}, {"type": "total", "display_text": "Total", "amount": 5000} ] }
Version 2026-01-23
In version 2026-01-23, discount amounts in totals[] are represented as positive integers.
Auto-applied item-level discount
// Request Example { "line_items": [ { "id": "li_1", "item": { "title": "Sneakers", "price": 10000 }, "quantity": 1 } ] }
// Response Example { "line_items": [ { "id": "li_1", "item": { "title": "Sneakers", "price": 10000 }, "quantity": 1, "totals": [ {"type": "subtotal", "amount": 10000}, {"type": "items_discount", "amount": 1000}, {"type": "total", "amount": 9000} ] } ], "discounts": { "applied": [ { "title": "10% Off Sitewide Sale", "amount": 1000, "automatic": true, "method": "each", "allocations": [ {"path": "$.line_items[0]", "amount": 1000} ] } ] }, "totals": [ {"type": "subtotal", "display_text": "Subtotal", "amount": 10000}, {"type": "items_discount", "display_text": "Item Discounts", "amount": 1000}, {"type": "total", "display_text": "Total", "amount": 9000} ] }
Auto-applied order-level discount
// Request Example { "line_items": [ ... ] }
// Response Example { "line_items": [ ... ], "discounts": { "applied": [ { "title": "$10 Off Orders Over $50", "amount": 1000, "automatic": true } ] }, "totals": [ {"type": "subtotal", "display_text": "Subtotal", "amount": 6000}, {"type": "discount", "display_text": "Order Promo", "amount": 1000}, {"type": "total", "display_text": "Total", "amount": 5000} ] }
API examples for user-applied promotions
The following examples demonstrate discounts triggered by a user entering a promotional code. The request payloads include the requested codes, and the responses echo them back while allocating the applied amounts.
Version 2026-04-08
In version 2026-04-08, discount amounts applied to the totals array use negative values.
Order-level discount
A flat discount applied to the order total. No allocations are needed; the discount applies to the order as a whole and uses type: "discount".
// Request Example { "line_items": [ ... ], "discounts": { "codes": ["SAVE10"] } }
// Response Example { "line_items": [ ... ], "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.
// Request Example { "line_items": [ ... ], "discounts": { "codes": ["SUMMER20"] } }
// Response Example { "line_items": [ { "id": "li_1", "item": { "title": "T-Shirt", "price": 2000 }, "quantity": 2, "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 code is invalid, it is echoed in codes but omitted from applied. The rejection is communicated using a warning in the messages[] array.
// Request Example { "line_items": [ ... ], "discounts": { "codes": ["SAVE10", "EXPIRED50"] } }
// Response Example { "line_items": [ ... ], "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 breakdowns. Note how priority and method define the calculation order.
// Request Example { "line_items": [ ... ], "discounts": { "codes": ["SUMMER20", "EXTRA5"] } }
// Response Example { "line_items": [ { "id": "li_1", "item": { "title": "T-Shirt", "price": 6000 }, "quantity": 1, "totals": [ {"type": "subtotal", "amount": 6000}, {"type": "items_discount", "amount": -1500}, {"type": "total", "amount": 4500} ] }, { "id": "li_2", "item": { "title": "Socks", "price": 4000 }, "quantity": 1, "totals": [ {"type": "subtotal", "amount": 4000}, {"type": "items_discount", "amount": -1000}, {"type": "total", "amount": 3000} ] } ], "discounts": { "codes": ["SUMMER20", "EXTRA5"], "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": "EXTRA5", "title": "Extra $5 Off", "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} ] }
Version 2026-01-23
In version 2026-01-23, discount amounts in totals[] and line_items[].totals[] are represented as positive integers, even though they represent a subtractive value to the receipt.
Order-level discount
// Request Example { "line_items": [ ... ], "discounts": { "codes": ["SAVE10"] } }
// Response Example { "line_items": [ ... ], "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)
// Request Example { "line_items": [ ... ], "discounts": { "codes": ["SUMMER20"] } }
// Response Example { "line_items": [ { "id": "li_1", "item": { "title": "T-Shirt", "price": 2000 }, "quantity": 2, "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
// Request Example { "line_items": [ ... ], "discounts": { "codes": ["SAVE10", "EXPIRED50"] } }
// Response Example { "line_items": [ ... ], "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
// Request Example { "line_items": [ ... ], "discounts": { "codes": ["SUMMER20", "EXTRA5"] } }
// Response Example { "line_items": [ { "id": "li_1", "item": { "title": "T-Shirt", "price": 6000 }, "quantity": 1, "totals": [ {"type": "subtotal", "amount": 6000}, {"type": "items_discount", "amount": 1500}, {"type": "total", "amount": 4500} ] }, { "id": "li_2", "item": { "title": "Socks", "price": 4000 }, "quantity": 1, "totals": [ {"type": "subtotal", "amount": 4000}, {"type": "items_discount", "amount": 1000}, {"type": "total", "amount": 3000} ] } ], "discounts": { "codes": ["SUMMER20", "EXTRA5"], "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": "EXTRA5", "title": "Extra $5 Off", "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} ] }
Error handling and user feedback
If a promo code or gift card offer cannot be applied, or if a previously applied offer is removed, your API must return clear reason codes. This ensures Google can display the appropriate feedback to the user.
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 must be informed.
Error code reference
Return the following canonical error codes within the messages array of your
response using "type": "warning" to communicate promotion eligibility.
| 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 |
Error payload examples
The following examples demonstrate how to construct the messages array for
common promotional code errors. Ensure all discount rejections use "type":
"warning".
Invalid code
{
"type": "warning",
"code": "discount_code_invalid",
"path": "$.discounts.codes[0]",
"content": "Promo code isn’t valid for this purchase."
}
Combined promotion errors
If a user applies a discount that cannot be combined with an existing
promotional code, or if adding a new code overrides and removes a previously
applied code, return a warning message.
{
"type": "warning",
"code": "discount_code_combination_disallowed",
"path": "$.discounts.codes[1]",
"content": "Promo codes can’t be combined. One or more codes were removed."
}
Merchant promo code limit reached
While the Google UI supports up to 10 codes per session, you may have stricter
limits on your backend (for example, allowing only 1 code per order). If a user
exceeds your store's specific promotional code limit, prevent the new code from
applying by returning a warning.
{
"type": "warning",
"code": "discount_code_combination_disallowed",
"path": "$.discounts.codes[1]",
"content": "No more promo codes can be applied for this purchase."
}
For a complete list of valid error codes and structures, see the Error code reference.