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. |
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. |
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)?
|
@raginpirate thanks for the review and the approval from @alexpark20 — two good catches here. 1. Agreed the current description is too prescriptive — happy to soften it. Before I update, I want to make sure I understand the scenario you're pointing to. My understanding has been that for CIT transactions with network tokens, the cryptogram and ECI are the expected verification mechanism — the cryptogram proves the token is being used by the authorized requestor for this specific transaction, and the ECI indicates the authentication level. Using CVV as an alternative to the cryptogram in a CIT flow is new to me. Is there a specific PSP or card network flow you're thinking of where that applies? Just want to make sure I get the description right and learn something in the process. Either way, I think the fix is to, as you suggested, simplify the description to something like: "When true, the handler requires card verification data (e.g., CVC for FPAN credentials)" — and leave it to the platform to provide the appropriate verification for their credential mode, rather than the constraint trying to enumerate fields per 2. Billing address — single enum vs. two booleans I like this. The two booleans create an awkward "ignored when" relationship and a 4th combination (street-only without postal code) that doesn't map to how AVS is actually used by issuers. A single constraint with enumerated values aligns better with the practical AVS modes merchants configure: "billing_address_requirement": {
"type": "string",
"enum": ["full", "postal_code"],
"description": "When present, specifies the billing address data the handler requires. 'full' requires a complete billing address. 'postal_code' requires a billing postal code for AVS verification. When absent, no billing address data is required."
}I'll hold off pushing changes until I hear back on #1 so I can address both together. |
|
@jamesandersen Check out VGS's documentation on it :) https://docs.verygoodsecurity.com/agentic-commerce/provision-network-tokens-for-payments#types-of-cryptograms
👍 totally agree! |
| | `brands` | array | — | Limit to specific card brands (e.g., `["visa", "mastercard"]`) | | ||
| | `requires_card_verification` | boolean | `false` | When `true`, the handler requires card verification data. For FPAN: CVC. For network tokens: cryptogram and ECI. | | ||
| | `requires_billing_address` | boolean | `false` | When `true`, the handler requires a billing address on the instrument. | | ||
| | `requires_billing_postal_code` | boolean | `false` | 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.
What do you think of merging this with required_billing_address?
If there was an enum for requires_billing_address_data: {FULL_ADDRESS, POSTAL_CODE, NONE}
It would avoid this situation where this property is ignored based on the value of requires_billing_address
There was a problem hiding this comment.
Yep - similar to the comment above - this is a good suggestion; I just updated the PR accordingly.
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.
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
Replace requires_billing_address (boolean) + requires_billing_postal_code (boolean) with requires_billing_address_data enum (full_address, postal_code, none). Soften requires_card_verification description to remove network token prescription per reviewer feedback.
1ef65d8 to
e6f8031
Compare
|
@raginpirate ahh ... I see you're talking about the short-form cryptogram that might be suitable for browser "autofill" network tokens - gotcha. @raginpirate / @kmcduffie - I've swapped to the enum based approach you both suggested. LMK if anything seems needed here (🤞 ) |
raginpirate
left a comment
There was a problem hiding this comment.
Sorry for a last round of nits here; expect significantly higher response time from me so we can close this out!
Rename requires_billing_address_data to billing_address_granularity and update description phrasing per reviewer feedback.
| "constraints": { | ||
| "brands": ["visa", "mastercard"], | ||
| "requires_card_verification": true, | ||
| "billing_address_granularity": "full_address" |
There was a problem hiding this comment.
The billing_address_granularity enum is doing a lot of work for a concept that
UCP already expresses precisely. postal_address.json defines nine named, all-optional fields:
street_address, extended_address, address_locality, address_region, address_country, postal_code, first_name, last_name, phone_number.
The three-level enum collapses all of that into an abstraction that doesn't map cleanly to what PSPs actually require, particularly internationally:
- US AVS needs
postal_code(full AVS addsstreet_address) - UK AVS needs
street_address+postal_code - Billing name matching needs
first_name+last_name full_addressis ambiguous on whether name fields are included
Rather than a granularity level, consider a field-set constraint that uses the vocabulary postal_address.json already defines:
"required_billing_address_fields": {
"type": "array",
"items": { "type": "string" },
"description": "Billing address fields the handler requires. Values correspond to field names in postal_address.json (e.g., 'postal_code', 'address_country', 'first_name')."
}| | Constraint | Type | Default | Description | | ||
| |---------------------------------|---------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ||
| | `brands` | array | - | Limit to specific card brands (e.g. `["visa", "mastercard"]`). | | ||
| | `requires_card_verification` | boolean | `false` | When `true`, the handler requires card verification data appropriate to the credential type (e.g., CVC for FPAN). | |
There was a problem hiding this comment.
Covered by others (@raginpirate, @alexpark20), but I agree that requires_card_verification feels off. It's a boolean with no direct linkage to the field it's actually requiring — the platform is left to "know" that this means cvc for FPAN. For network tokens and DPANs, cryptogram and eci_value are structurally required regardless, so the boolean doesn't meaningfully apply.
Are there other examples you're aiming to solve for with requires_card_verification? Otherwise I think we can lean towards directly addressing the CVC case. See below for an idea on possible structuring.
| "default": false, | ||
| "description": "When true, the handler requires card verification data appropriate to the credential type (e.g., CVC for FPAN)." | ||
| }, | ||
| "billing_address_granularity": { |
There was a problem hiding this comment.
Bringing it all together: The two new fields point toward a pattern worth making explicit. Rather than top-level boolean/enum fields on constraints, what if constraints used sub-objects that mirror the instrument's own property hierarchy?
Instead of:
"constraints": {
"requires_card_verification": true,
"billing_address_granularity": "postal_code"
}
Something like:
"constraints": {
"credential": {
"card_credential": {
"required": ["cvc"]
},
"bank_account_credential": {
"required": ["account_type"]
}
},
"billing_address": {
"required": ["postal_code"]
}
}
The credential key mirrors the property on the instrument; the card_credential sub-key handles the polymorphic case, constraints are scoped to a specific subtype and can't bleed across incompatible schemas.
The required arrays use field names from the schemas they govern (card_credential.json, postal_address.json). No new vocabulary to maintain, no translation layer between granularity enum values and actual address fields.
There was a problem hiding this comment.
@raginpirate curious if this fits the constraints model you had in mind in #187.
|
@gsmith85 thanks for the detailed review! I want to make sure you have the full context on the discussion arc — I did force push and update the PR content mid-way through the discussion here, which makes it harder to follow the evolution from the diff alone. The comment thread tells the fuller story. This PR originally proposed So the current approach — flat named constraints — is the result of iterating away from the field-reference pattern toward something the maintainers felt was more consistent with the existing constraints model. On your specific points:
Structured sub-object constraints — This is the most interesting suggestion, but it's also the closest to what Happy to iterate further if @raginpirate or @alexpark20 feel any of these proposals better serve the constraints direction. The goal has been to keep this PR tight and land incremental value, with room for richer constraint modeling in follow-ups. |
|
Love the thinking here @gsmith85!
Great push on this one; originally I also thought about just having a full-resolution answer of using billing_address and then specifying required fields under it, but I swayed away from it for a more payments-focused abstraction that matches how providers generally solve this problem; full, postal-only, and not required; keeps it simple. I do think this works for most if not all merchants. But seeing this push again echo'd by you @gsmith85 makes me wonder more about constraints generally across UCP. I think it is right for us to build this as constraints for addresses, and to share this constraint primitive with other domains. Looking around, it seems like fulfillment actually has a gap where it does not expose any concept of address constraints, and we could be building a reasonable foundation here! With this we also future-proof ourselves; we don't have to maintain some payments constraint abstraction for addresses. TLDR; agreed with this push, and we have an action to take back to the UCP TC to discuss how we share this into the fulfillment extension as an example 😄
For this one, agreed and we've discussed available_credentials needing to be advertised as well in UCP at some point. The nuance here is that although technically we want to describe this requirement about the raw credential here, you can have "credential inception" by which a provider token is what you support ingesting (no cvv field on it for example), but yet we're trying to communicate that we need a cvv on the raw card behind that credential. As well, you would have to redefine the same constraint N times for each credential type. I see two options here:
I did some pairing with Claude and I opened this: #424 <-- PTAL @gsmith85 and @jamesandersen! Maybe we can just shift to a better state of the world with the abstract fundamentals here. |
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