Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 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
232 changes: 231 additions & 1 deletion docs/specification/checkout-rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,230 @@ Follow-up calls after initial `fulfillment` data to update selection.
}
```

#### Update Payment

Prior to completing checkout, the Platform may provide the Business with selected payment instrument hints. These hints allow the Business to apply to the checkout session the expected benefits the selected payment instrument qualifies the Buyer for.

=== "Request"

```json
PUT /checkout-sessions/{id} HTTP/1.1
UCP-Agent: profile="https://platform.example/profile"
Content-Type: application/json

{
"id": "chk_123456789",

Choose a reason for hiding this comment

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

The Update Payment example uses PUT /checkout-sessions/{id} and includes "id": "chk_123456789" in the body, but the spec section doesn’t state what happens if the path {id} and body id differ. Add a normative rule: either (a) body id MUST match path {id} and mismatches MUST be rejected with a specific error, or (b) body id is ignored and the path is authoritative.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great call-out. Because this PR feat did not introduce that potential conflict (i.e. there are other Update docs with this issue), I propose we create a separate issue to address that. Thoughts?

Thinking through the solution, it's probably to return a 400 or 422. And this would then be captured under https://ucp.dev/2026-01-23/specification/checkout-rest/#status-codes .

Choose a reason for hiding this comment

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

Sounds good. Please create a separate issue.

"buyer": {
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe"
},
"line_items": [
{
"item": {
"id": "item_123",
"title": "Red T-Shirt",
"price": 2500
},
"id": "li_1",
"quantity": 2
}
],
"fulfillment": {
"methods": [
{
"id": "shipping_1",
"type": "shipping",
"line_item_ids": ["item_123"],
"selected_destination_id": "dest_home",
"destinations": [
{
"id": "dest_home",
"street_address": "123 Main St",
"address_locality": "Springfield",
"address_region": "IL",
"postal_code": "62701",
"address_country": "US"
}
],
"groups": [
{
"id": "package_1",
"selected_option_id": "express"
}
]
}
]
},
"payment": {
"instruments": [
{
"id": "pi_gpay_5678",
"handler_id": "gpay_1234",
"type": "card",
"selected": true,
"display": {
"brand": "mastercard",
"last_digits": "5678",
"rich_text_description": "Google Pay •••• 5678"
},
"qualifiers": [
"com.example.tender_a"
]
}
]
}
}
```

=== "Response"

```json
HTTP/1.1 200 OK
Content-Type: application/json

{
"ucp": {
"version": "2026-01-11",
"capabilities": {
"dev.ucp.shopping.checkout": [
{"version": "2026-01-11"}
]
},
"payment_handlers": {
"com.google.pay": [
{
"id": "gpay_1234",
"version": "2026-01-11",
"config": {
"allowed_payment_methods": [
{
"type": "CARD",
"parameters": {
"allowed_card_networks": ["VISA", "MASTERCARD", "AMEX"]
}
}
]
}
}
]
}
},
"id": "chk_123456789",
"status": "ready_for_complete",
"currency": "USD",
"line_items": [
{
"id": "li_1",
"item": {
"id": "item_123",
"title": "Red T-Shirt",
"price": 2500
},
"quantity": 2,
"totals": [
{"type": "subtotal", "amount": 5000},
{"type": "total", "amount": 5000}
]
}
],
"buyer": {
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe"
},
"totals": [
{
"type": "subtotal",
"amount": 5000
},
{
"type": "tax",
"amount": 400
},
{
"type": "total",
"amount": 5400
}
],
"links": [
{
"type": "terms_of_service",
"url": "https://merchant.com/terms"
}
],
"fulfillment": {
"methods": [
{
"id": "shipping_1",
"type": "shipping",
"line_item_ids": ["item_123"],
"selected_destination_id": "dest_home",
"destinations": [
{
"id": "dest_home",
"street_address": "123 Main St",
"address_locality": "Springfield",
"address_region": "IL",
"postal_code": "62701",
"address_country": "US"
}
],
"groups": [
{
"id": "package_1",
"line_item_ids": ["item_123"],
"selected_option_id": "express",
"options": [
{
"id": "standard",
"title": "Standard Shipping",
"description": "Arrives in 5-7 business days",
"totals": [
{
"type": "total",
"amount": 500
}
]
},
{
"id": "express",
"title": "Express Shipping",
"description": "Arrives in 2-3 business days",
"totals": [
{
"type": "total",
"amount": 1000
}
]
}
]
}
]
}
]
},
"payment": {
"instruments": [
{
"id": "pi_gpay_5678",
"handler_id": "gpay_1234",
"type": "card",
"selected": true,
"display": {
"brand": "mastercard",
"last_digits": "5678",
"rich_text_description": "Google Pay •••• 5678"
},
"qualifiers": [
"com.example.tender_a"
]
}
]
}
}
```

### Complete Checkout

If businesses have specific logic to enforce field existence in `buyer` and
Expand Down Expand Up @@ -730,6 +954,9 @@ place to set these expectations via `messages`.
"card_art": "https://cart-art-1.html",
"description": "Google Pay •••• 5678"
},
"qualifiers": [
"com.example.tender_a"
],
"billing_address": {
"street_address": "123 Main St",
"address_locality": "Anytown",
Expand Down Expand Up @@ -892,7 +1119,10 @@ place to set these expectations via `messages`.
"brand": "mastercard",
"last_digits": "5678",
"rich_text_description": "Google Pay •••• 5678"
}
},
"qualifiers": [
"com.example.tender_a"
]
}
]
}
Expand Down
13 changes: 7 additions & 6 deletions docs/specification/checkout.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,13 @@ ELSE IF requires_buyer_review is not empty
Standard errors are standardized error codes that platforms are expected to
handle with specific, appropriate UX rather than generic error treatment.

| Code | Description |
| :----------------------- | :------------------------------------------------------------------------- |
| `out_of_stock` | Specific item or variant is unavailable |
| `item_unavailable` | Item cannot be purchased (e.g. delisted) |
| `address_undeliverable` | Cannot deliver to the provided address |
| `payment_failed` | Payment processing failed |
| Code | Description |
| :---------------------- | :------------------------------------------------------------------------- |
| `out_of_stock` | Specific item or variant is unavailable |
| `item_unavailable` | Item cannot be purchased (e.g. delisted) |
| `address_undeliverable` | Cannot deliver to the provided address |
| `payment_failed` | Payment processing failed |
| `invalid_qualifier` | Submitted payment did not qualify for hinted qualifiers |

Businesses **SHOULD** mark standard errors with `severity: recoverable` to
signal that platforms should provide appropriate UX (out-of-stock messaging,
Expand Down
20 changes: 20 additions & 0 deletions docs/specification/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,26 @@ within UCP: **Negotiation**, **Acquisition**, and **Completion**.
2. **Acquisition (Platform ↔ Payment Credential Provider):** The platform executes the handler's logic. This happens client-side or agent-side, directly with the payment credential provider (e.g., exchanging credentials for a network token). The business is not involved, ensuring raw data never touches the business's frontend API.
3. **Completion (Platform → Business):** The platform submits the opaque credential (token) to the business. The business uses it to capture funds via their backend integration with the payment credential provider.

### Payment Qualifiers

Choose a reason for hiding this comment

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

Consideration: Is qualifiers descriptive enough? "Qualifiers" often implies a binding state or a restrictive limitation. Since the semantics explicitly define these as non-binding "hints" that require independent verification, benefit_hints or eligibility_hints might be a better fit. It signals that the field is a suggestion for the presentation and pre-checkout logic rather than a final, validated qualification for financial settlement.


Prior to completing checkout, the Platform **MAY** provide the Business with selected payment instrument *hints*. These hints allow the Business to apply to the checkout session the expected benefits the selected payment instrument qualifies the Buyer for. This gives the user a more accurate preview of the final order total before they commit to the purchase.

Payment instruments **MAY** include a `qualifiers` array: opaque, namespaced strings that hint benefit eligibility associated with the selected instrument. The meaning of qualifier values, and how they are derived, is communicated between the Business and Platform out of band (for example, via offline agreement on BIN ranges or program identifiers).

Choose a reason for hiding this comment

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

Consideration: Will this field exclusively be used for "benefits"?

While the current description focuses on perks like discounts, there are several
scenarios where a Platform might pass hints for regulatory or technical restriction
that aren't "benefits" to the user. For example:

  • Product-Category Restrictions: Signaling a corporate or government card that is legally barred from purchasing specific items (e.g., alcohol, tobacco, or gaming).
  • Surcharge Eligibility: Signaling a high-cost commercial card where a merchant is legally allowed (or required) to pass on a processing fee.
  • KYC/Identity Status: Signaling a specific "Know Your Customer" verification level that might skip—or trigger—additional identity checks.
  • Cross-Border Compliance: Signaling an instrument with geographic limitations that might affect tax or shipping legality.

If we anticipate these non-benefit use cases, would a more neutral term like eligibility_hints be more appropriate?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for this input. I went with eligibility_hints.


**Qualifier Strings:**

- Qualifier strings **SHOULD** use reverse-domain naming to avoid collisions.
- Qualifiers strings **MUST NOT** contain sensitive payment attributes such as PAN, BIN, PII, or user-unique identifiers.

Choose a reason for hiding this comment

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

Nit: We are not at all consistent, but define acronyms like BIN (Bank Identification Number)upon their first mention? It helps ensure the spec remains accessible to engineers who might be new to the payments domain.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree. We have a glossary, but it's mostly for our own terms. Maybe separately we take a pass at capturing all acronyms there?

Copy link

Choose a reason for hiding this comment

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

Primarily as a smoke test to make sure that I had the CLA signed, I split out a central glossary (#241). We can layer these acronyms in there once it's merged!

- Qualifiers strings **SHOULD** be coarse-grained program identifiers.

**Qualifier Semantics:**

- Qualifiers are hints and **MUST NOT** be treated as proof of eligibility.
- The Business **MUST NOT** grant final or irreversible benefits solely due to qualifiers.
- The Business **MUST** determine benefit eligibility from the completion payment instrument and credential, not from qualifiers.
- The Business **SHOULD** return an error, using the [`invalid_qualifier`](checkout.md#standard-errors), during checkout completion if the provided qualifiers did not match the final payment instrument's eligibility.
- When receiving `invalid_qualifier`, the Platform **SHOULD** update qualifiers, and **MUST** present the user with an opportunity to review benefits changes (e.g. discounts, totals, etc.).

### Payment Handlers

Payment Handlers are **specifications** (not entities) that define how payment
Expand Down
3 changes: 2 additions & 1 deletion source/schemas/shopping/types/error_code.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"out_of_stock",
"item_unavailable",
"address_undeliverable",
"payment_failed"
"payment_failed",
"invalid_qualifier"
]
}
10 changes: 10 additions & 0 deletions source/schemas/shopping/types/payment_instrument.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@
"display": {
"type": "object",
"description": "Display information for this payment instrument. Each payment instrument schema defines its specific display properties, as outlined by the payment handler."
},
"qualifiers": {

Choose a reason for hiding this comment

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

Qualifiers is currently an unbounded array of unbounded strings. For a protocol-level schema used by autonomous agents, this is a straightforward DoS / log-injection / storage-amplification risk.

Add schema constraints, for example:

  • maxItems (e.g., 10–50 depending on expected use)
  • uniqueItems: true (if semantics are set-like)
  • string constraints like maxLength (e.g., 128/256) and a conservative pattern (e.g., reverse-domain-ish allowed chars)

Even if implementations don’t enforce strictly, having constraints in the canonical schema sets a safe baseline.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How should we communicate the reverse-domain-ish characters?

Shall we say items match the pattern of ^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+\.[a-z][a-z0-9_-]*$ ? i.e. {reverse-domain}.{service}.{qualifier}, e.g. "com.target.red_card" or "co.uk.example.qualifier-abc123"

Copy link
Contributor

Choose a reason for hiding this comment

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

re: max items and string constraints; I feel like those needs to be applied across the board to UCP and are not today. We should discuss this in the TC; do we need a sweeping application of that to the spec, and should that be done in isolation of this PR?

I also disagree with enforcing reverse-dns here; from my POV the primary use case of this should be in-band by payment handlers to express how you negotiate qualfiier context that you want exposed by a payment handler for a specific instrument. Out-of-band use cases should not be dictated and require reverse-dns; they have their own solutions for identifying the relying party they negotiated with and interpreting those fields independently for those parties.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree we have an opportunity to apply these across the board. I am okay adding the constraint here (as I have done), or removing it an referencing this feedback in a new issue that takes a broader look.

Out-of-band use cases should not be dictated and require reverse-dns

Without reverse domain naming, there's a significant chance of collision. Imagine two entities with member_program; it would be helpful for the program named be tied to that program owner, e.g. com.target.member_program so that another merchant can also have com.merchant_b.member_program.

"type": "array",
"description": "Opaque, namespaced qualifier strings from the Platform that hint to the Business the benefits to apply at checkout time, based on the selected payment instrument.",
"uniqueItems": true,
"maxItems": 99,
"items": {
"type": "string",
"maxLength": 256
}
}
},
"additionalProperties": true,
Expand Down
Loading