-
Notifications
You must be signed in to change notification settings - Fork 295
feat: add payment instrument qualifiers #214
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
17163ab
9882035
607c229
516841f
7a26d71
37beca0
00f586f
76ff335
3e371ef
6c3f7de
3ba3107
c657203
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
ACSchil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Content-Type: application/json | ||
|
|
||
| { | ||
| "id": "chk_123456789", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 . There was a problem hiding this comment. Choose a reason for hiding this commentThe 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": [ | ||
| "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 | ||
|
|
@@ -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", | ||
|
|
@@ -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" | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
ACSchil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| **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.). | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
|
|
||
| ### Payment Handlers | ||
|
|
||
| Payment Handlers are **specifications** (not entities) that define how payment | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should come at this explanation as a feature of payment handlers, NOT from the perspective of a black hole platforms can leverage for out-of-band solutions.
As in, my payment handler lets me negotiate available qualifiers, and exposes that context to the agent to submit to the platform. It just so happens to give platforms and businesses an easy path to do out-of-band context sharing, but IMO we shouldn't be authoring this from that perspective.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand this POV. See #214 (comment) . Are you okay with us moving forward with this for now?