Promo codes and discounts

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-readable title, the positive amount of the discount, and the calculation method.
  • Rejections (messages): If a code is invalid, omit it from the applied array and communicate the failure reason using a canonical warning in the messages array.

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 in totals[] and line_items[].totals[] must use negative values to reflect their subtractive effect on the receipt.
  • Version 2026-01-23: Discount amounts in totals[] and line_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[].amount must equal the applied_discount.amount.
  • Line-item totals sum: totals[type=items_discount].amount must equal the sum of line_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.