-
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 8 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" | ||
| }, | ||
| "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 | ||
|
|
@@ -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", | ||
|
|
@@ -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" | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
ACSchil marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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
|
||
|
|
||
| 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). | ||
|
||
|
|
||
| **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. | ||
|
||
| - 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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": { | ||
|
||
| "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, | ||
ACSchil marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| "items": { | ||
| "type": "string", | ||
| "maxLength": 256 | ||
| } | ||
| } | ||
| }, | ||
| "additionalProperties": true, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.