feat: add card verification and billing address constraints#288
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
69a3cd0 to
de0b8e6
Compare
There was a problem hiding this comment.
I understand the motivation here, but I am concern that this solution is not scalable, especially for any new schema type to be introduced. I think the right solution is to leverage the .schema field in entity (ucp.json#/$defs/entity).
Before PR #49 , payment handlers had separate config_schema (URI) and instrument_schemas (array of URIs). PR #49 consolidated these into a single schema URI — but this was a simplification of the reference structure, not a reduction in capability. A single JSON Schema document can contain $defs covering all instrument types and their requirements. That said, I think the protocol's existing schema field is already capable of expressing everything required_fields is trying to express, and more.
Although, one can argue that the current schema in the entity (ucp.json#/$defs/entity) cannot really express its intention. I imagine people often assume that schema is only for the payment handler config. As an alternative to introducing required_fields, I think re introducing instrument_schema (just URI, not array though) could solve the problem. What do you think?
|
@alexpark20 thanks for taking time to do the review! The context on #49 is useful. I agree that While I think I could get behind this idea here's a couple additional thoughts for why the
To make it concrete — here's the Shopify card handler under each approach:
{
"id": "shopify.card",
"version": "2026-01-15",
"schema": "https://shopify.dev/ucp/card-payment-handler/2026-01-15/config.json",
"available_instruments": [{
"type": "card",
"constraints": { "brands": ["visa", "master", "american_express"] },
// business can vary the required fields without serving a different handler schema
"required_fields": ["credential.cvc"]
}]
}
// https://shopify.dev/ucp/card-payment-handler/2026-01-15/instrument-requirements.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"allOf": [
// need to pull this schema...
{ "$ref": "https://ucp.dev/schemas/shopping/types/card_payment_instrument.json" },
{
"properties": {
"credential": {
"allOf": [
// ... and this one to get the complete picture of the type refinement
{ "$ref": "https://ucp.dev/schemas/shopping/types/card_credential.json" },
// probably requires a different schema when using this handler definition
// where CVC isn't required or zip code IS required
{ "required": ["cvc"] }
]
}
}
}
]
}Again - I think either way could work and I'm still getting calibrated on what best serves the broader community but hopefully this helps flesh out a bit more of the motivation for the |
de0b8e6 to
ba16c3e
Compare
|
@jamesandersen , thanks for the detailed response and the examples! It really helps me see the full motivation. The per-merchant variance point makes sense, and I think it actually clarifies why we need both required_fields and the static schema rather than one or the other. I agree that The one gap I see in
Note that the platform already fetches and evaluates the handler during the discovery, so there's no additional operational overhead. This gives the platform a static, structural definition of what the handler's instrument looks like. Here is how I picture both plays its role: Similar to how buyer.json (or any other type in the schema) work today, fields are declared as optional unless they are strongly required by the handler itself (e.g. a handler that fundamentally cannot process without a billing address would mark it required). Everything else stays optional in the schema, and per-merchant requirements are communicated at checkout time via As suggestion for this PR, would you open to add more language in the doc? I am okay either deferring the decision of introducing a separate schema field in another PR or combining it as part of this PR. |
01e3cae to
463631f
Compare
|
@alexpark20 thanks for the thoughtful review and the context on PR #49! I've updated this PR to incorporate your suggestion — the schema now includes both
I've also added normative language per your suggestion:
The PR description and docs have been updated to reflect both properties and how they complement each other. Let me know what you think! |
|
@jamesandersen @alexpark20 -- great discussion here! ❤️ I want to bring my perspective on this.
In #187 we only pushed forward with instruments for the time being to apply constraints against. I heavily considered putting available_credentials on available_instruments which would allow this description at the right layer, but I was not confidant in my abstraction ahead of adoption. My main concern; if your handler supports a few different tokenization strategies, the concept of So... on this topic, I'm not sure I see a gap this PR is currently solving. Instead, I'd frame this PR as searching for the right way to unify and distill constraint requirements and challenging if the current state is good enough, which it might be (similar goal as #187 actually!). WDYT? |
|
@raginpirate thanks for the thoughtful pushback — the tokenization example is a great illustration of why this is tricky. The core tension is: merchants need to express per-checkout (not per schema) requirements, but instruments and credentials are modeled as separate structures in UCP, so some requirements are naturally instrument-level (billing address) while others span the credential boundary (card verification). I considered narrowing I think the cleaner path is to standardize on
This gives us a single mechanism, consistent with #187. The tradeoff is that constraint semantics become implicit per credential scheme — If this direction works for you and @alexpark20, I'll update the PR accordingly. |
|
@jamesandersen Based on your comment, I was reviewing the |
|
@kmcduffie good question — you're right that
So the hierarchy tells the platform that a credential is attached, but since it's the same Full disclosure — this unified modeling initially threw me as well. In PR #296 I proposed a separate Quick illustration: {
"available_instruments": [{
"type": "card",
"constraints": {
"brands": ["visa", "mastercard"],
"requires_card_verification": true
}
}]
}The platform sees |
|
Ah, I missed the purpose of constraints here and this is a good learning! thank you @raginpirate ! After reading comments and new context, here some thoughts: On required_fields vs. constraints I can see how That said, I think the implicit per-credential-scheme semantics (e.g., requires_card_verification meaning CVC for FPAN but cryptogram for network tokens) exposes a few gaps in the current modeling.
From my understanding, the current type-specific mapping (card instrument → card credential) is convention, not schema enforced. When we start putting credential level requirements on instrument level constraints (like As you mentioned already @raginpirate , should we consider introducing
Right now cvc, cryptogram, and eci_value are flat siblings on "verification": {
"type": "object",
"description": "Card verification data. Required fields depend on card_number_type."
"properties": {
"cvc": {
"type": "string",
"maxLength": 4,
"description": "Card verification code. Applicable to fpan."
},
"cryptogram": {
"type": "string",
"description": "Cryptogram provided with network tokens."
},
"eci_value": {
"type": "string",
"description": "Electronic Commerce Indicator provided with network tokens."
}
}
}
With
{
"if": { "properties": { "card_number_type": { "const": "fpan" } } },
"then": { "properties": { "verification": { "required": ["cvc"] } } }
},
{
"if": { "properties": { "card_number_type": { "const": "network_token" } } },
"then": { "properties": { "verification": { "required": ["cryptogram", "eci_value"] } } }
}This way, On instrument_schema @raginpirate , I get your point, and that's fair, and this isn't a hill I'll die on. But I do want to make sure we're making this call with good reasoning. The current schema description on entity is "URL to JSON Schema defining this entity's structure and payloads", which doesn't clearly indicate it will include input schema for payment instruments when using this particular handler. In practice, the platform and business fetch the handler schema to build/verify the payment_handlers response in both well-known and checkout calls. An |
|
Thanks @alexpark20 — the Would folks be open to landing this PR as a pragmatic step forward? Concretely, that would mean adding fields like I'd update this PR to drop |
|
Sorry for the very long circle back on this one @jamesandersen @alexpark20. @jamesandersen I'm aligned with your proposal to add a few extra constraints for quick product wins 🚀 @alexpark20 responding to your thoughts, I love seeing you dive into this domain 🤿
I think I get what you are saying, but how would we think about structuring that? 1 handler has N instrument schemas, so I view this just like how a capability defines N payload schemes under it; the problem seems to be the same there. I will not act as a blocker over the delivery of this if integrators agree this is important to optimize how they type-check and type-share across UCP, but I think this is not a problem isolated to payments... maybe we can chat about this problem deeper if you view one.
Yeah this one is fair and makes instruments and credentials more expressive 😄 Its not a prioritized investment because most handlers we've looked at really just use a handful of instruments with single credentials right now, but happy to see that opened independently with a clear set of examples we're empowering and/or a real adopter!
I think this schema proposal is fair but it doesn't match the shape industry payments APIs; folks keep those values flat on the card object, and I think you can still write a constraint regardless of how it's nested. I would advocate to not take on this refactor. |
Adds named constraints to card_payment_instrument.json for per-merchant requirements that vary at checkout time: - requires_card_verification: when true, platform must collect CVC (FPAN) or cryptogram/ECI (network tokens) - requires_billing_address: when true, platform must collect billing address - requires_billing_postal_code: when true, platform must collect billing postal code for AVS verification Uses the existing constraints mechanism on available_instruments, consistent with how brands constraints already work. Constraints participate in the existing resolution flow and can vary per merchant without changing the handler schema. Docs updated with Card Constraints section in payment-handler-guide.md.
c543d1c to
22b4708
Compare
|
PR updated — dropped
Rebased on latest main, docs updated. Awaiting any further review or, if aligned, approval to merge. @raginpirate @alexpark20 |
|
Thanks for the updates @jamesandersen, and @raginpirate for the thoughts on my earlier point 👏 I am also aligned with landing this as named constraints Additional notes: @raginpirate — fair point on instrument_schema, it is not isolated to payments. We can take that to a separate conversation if it turns out to be a real problem worth solving across the protocol 👍 On the verification object, I am not fully agree with the idea that constraints work regardless of how fields are nested. With the current flat structure, Although, your point about matching industry payment API shapes is fair, and I don't think it needs to be solved here as part of this PR. |
Address review feedback from @alexpark20: - Add `default: false` to each requires_* constraint in the schema - Add Default column to the constraints table in docs - Add normative language clarifying that constraints are additive to schema requirements — a `requires_*` constraint of false or absent MUST NOT be interpreted as overriding a schema-required field
alexpark20
left a comment
There was a problem hiding this comment.
Thank you for iterating and addressing all the feedbacks 👏
raginpirate
left a comment
There was a problem hiding this comment.
great work, just sharing two notes about the constraints which feel odd.
| "requires_card_verification": { | ||
| "type": "boolean", | ||
| "default": false, | ||
| "description": "When true, the handler requires card verification data. For FPAN: CVC. For network tokens: cryptogram and ECI." |
There was a problem hiding this comment.
For network tokens: cryptogram and ECI
This constraint description is a bit odd to me, because network tokens can be valid without either of these; a network token can use cvv. Might be worth focusing this only on the requirement of cvv for fpans in specific, and leaving network tokens as open to the platform to make the right decision on.
| "requires_billing_address": { | ||
| "type": "boolean", | ||
| "default": false, | ||
| "description": "When true, the handler requires a billing address on the instrument." | ||
| }, | ||
| "requires_billing_postal_code": { | ||
| "type": "boolean", | ||
| "default": false, | ||
| "description": "When true, the handler requires a billing postal code for AVS verification. Ignored when requires_billing_address is true." | ||
| } |
There was a problem hiding this comment.
Is this better to represent as an address constraint with three example values (full_address, postal_code, nil)?
Enhancement Proposal: Card Payment Constraints
Summary
Adds named constraints to
card_payment_instrument.jsonthat enable merchants to communicate per-checkout instrument requirements to platforms. This gives platforms a clear signal of what the merchant requires — no guesswork, no trial-and-error tokenization.Motivation
Many instrument and credential properties are optional at the protocol level but required by individual merchants and their PSPs. For example, CVC is optional in
card_credential.jsonbut most merchants require it for card-not-present transactions. A handler likedev.shopify.cardserves thousands of merchants — some require CVC, some don't (it's a per-merchant fraud setting). Without a way to communicate these requirements, the platform must guess which optional fields the merchant actually needs.Design
Uses the existing
constraintsmechanism onavailable_instruments, consistent with howbrandsconstraints already work. Constraints participate in the existing resolution flow and can vary per merchant without changing the handler schema.New Constraints
requires_card_verificationtrue, the handler requires card verification data. For FPAN: CVC. For network tokens: cryptogram and ECI.requires_billing_addresstrue, the handler requires a billing address on the instrument.requires_billing_postal_codetrue, the handler requires a billing postal code for AVS verification. Ignored whenrequires_billing_addressistrue.Example
{ "available_instruments": [ { "type": "card", "constraints": { "brands": ["visa", "mastercard"], "requires_card_verification": true, "requires_billing_postal_code": true } } ] }Per-
card_number_typeSemanticsThe
requires_card_verificationconstraint adapts to the credential mode:card_number_typefpancvcnetwork_tokencryptogram,eci_valueThe spec documents these mappings. A future PR could make the schema fully self-documenting via a
verificationobject with conditionals percard_number_type(as discussed in PR review), but named constraints provide a pragmatic step forward today.Code Changes
Modified Files:
source/schemas/shopping/types/card_payment_instrument.json— Addedrequires_card_verification,requires_billing_address, andrequires_billing_postal_codeto theavailable_card_payment_instrumentconstraintsdocs/specification/payment-handler-guide.md— Added Card Constraints section with constraint table, example, and resolution flow documentationTest Plan
card_payment_instrument.jsonavailable_instrumentsReferences
Type of change
Checklist