-
Notifications
You must be signed in to change notification settings - Fork 305
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
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.
Contributor
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": [ | ||
|
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. What do we think of using
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. 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 |
||
| "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?
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. Great question Amit and I will actually drive this further while thinking through the full flow:
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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,15 @@ | |
| "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": { | ||
|
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. 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. 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. 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 In the 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, | ||
| "items": { | ||
| "type": "string", | ||
| "maxLength": 256 | ||
| } | ||
| } | ||
| }, | ||
| "additionalProperties": true, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.