Skip to content
Closed
Show file tree
Hide file tree
Changes from 11 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"
},
"eligibility_hints": [
Copy link
Contributor

Choose a reason for hiding this comment

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

What do we think of using context for this instead, introduced in https://github.com/Universal-Commerce-Protocol/ucp/pull/62/changes? It feels like that slice of the object is meant to give us a home for these kinds of up front "hints", and its documentation specifically references custom pricing, and the higher-resolution fields in the API that would override the hinting. It feels a little awkward to require the partner to provide so much of the payment instrument, just to offer up a hint on what payment-related qualifiers will apply.

Copy link
Contributor

Choose a reason for hiding this comment

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

Honestly great question @lemonmade. I think there is serious merit in just using context.

To argue the point of this PR though, I do see that qualifiers are tied to an instrument, so in the case of split payments it becomes harder to justify using a default checkout context bucket; which instrument should be visualized as contributing which qualifier(s)?

And the deeper reason I would prefer to push for qualifiers, is because I do want the primitive to be usable by payment handler developers, rather than it being this arbitrary bucket its being first introduced as here. Example: Use the company_name_pay sdk, and have it expose an event similar to address selection for method selection, which lets the platform push an instrument with qualifier pay_1234 to describe the matching bin range it was initialized with. If this is the future we want to support, the primitive living on the instrument does make sense to me, as its meant for handlers to interface with, rather than checkout's generic context bucket.

"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"
},
"eligibility_hints": [
"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"
},
"eligibility_hints": [
"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"
}
},
"eligibility_hints": [
"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_eligibility_hint` | Submitted payment is not eligible for hinted benefits |

Businesses **SHOULD** mark standard errors with `severity: recoverable` to
signal that platforms should provide appropriate UX (out-of-stock messaging,
Expand Down
28 changes: 28 additions & 0 deletions docs/specification/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,34 @@ 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 Eligibility Hints

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.

**Example Benefits:**

- Co-branded Card Perks (e.g., instant discounts for using a specific brand's card).
- Issuer-Funded Promotions (e.g., statement credits).
- FX Fee Waivers for international transactions.
- Promotional APRs or specialized financing terms.
- Logistical Upgrades (e.g., expedited shipping for premium cardholders).

Payment instruments **MAY** include an `eligibility_hints` array: opaque, namespaced strings that hint benefit eligibility associated with the selected instrument. The meaning of `eligibility_hints` 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).

**eligibility_hints Strings:**

- `eligibility_hints` strings **SHOULD** use reverse-domain naming to avoid collisions.
- `eligibility_hints` strings **MUST NOT** contain sensitive payment attributes such as PAN, BIN, PII, or user-unique identifiers.
- `eligibility_hints` strings **SHOULD** be coarse-grained program identifiers.

**eligibility_hints Semantics:**

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

Choose a reason for hiding this comment

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

So does this mean, the platform should ask the user to select a different payment instrument or remove the eligibility_hint from their request and use the same instrument or are there other options for the platform?
I am trying to understand what are the valid responses from Platform once Business returns an error "invalid_eligibility_hint" and should we update the documentation to be clearer?

Copy link
Contributor

@raginpirate raginpirate Mar 5, 2026

Choose a reason for hiding this comment

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

Great question Amit and I will actually drive this further while thinking through the full flow:

  • As a platform, I submit instrument 1234 with a qualifier the business does not respect. I am expecting the business can actually silently ignore it, or optionally provide back a message to say "hey this qualifier isn't right so I removed it". The state can still be ready-for-submit immediately, not requiring platform action. The platform is informed regardless because the qualifier is not surfaced back.
  • As a business, when I receive a credential for an instrument with qualifier 1234, but now the credential mismatches from that qualifier context, I can perform the same operation: remove the qualifier, optional non-blocking message back to the platform.
  • If this were to occur during a complete call, and this actually impacts the totals, I should fail the complete. It should not be a requirement to fail though if the qualifier never meaningfully impacted the state of the checkout; that should be up to business discretion, and I am not sure needs to be reflected in the overview of this feature.

Is all of the above correct? If so, I'd love to see it (or the adjustments to that view) codified with the right MUST / SHOULD outlook.


### 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_eligibility_hint"
]
}
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."
},
"eligibility_hints": {
Copy link
Contributor

@raginpirate raginpirate Mar 5, 2026

Choose a reason for hiding this comment

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

Nit: The phrase "eligibility_hints" feels like its conveying what this instrument can be used for (especially confusing for me after working on constraints) and I'm not sure it has any industry examples of the term being used. I preferred the original title, or some other title along the lines of "something_hint", but this is non-blocking.

Copy link

@gsmith85 gsmith85 Mar 5, 2026

Choose a reason for hiding this comment

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

With the feedback from @raginpirate on naming and to push on the idea that these might not strictly be benefits (per #214 (comment)) perhaps consider provisions? Or, provision_hints if you want to more strictly communicate it's a hint.

In the description, and elsewhere we can note provisions may capture hints for "entitlements" (e.g. priority_fulfillment, travel_credit) and "constraints" (e.g. no_tobacco, domestic_only).

Also non-blocking, but obviously this modeling is easiest to change early.

"type": "array",
"description": "Opaque, namespaced eligibility hint 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