Skip to content

feat: add card verification and billing address constraints#288

Open
jamesandersen wants to merge 2 commits intoUniversal-Commerce-Protocol:mainfrom
jamesandersen:feat/required-fields-available-instruments
Open

feat: add card verification and billing address constraints#288
jamesandersen wants to merge 2 commits intoUniversal-Commerce-Protocol:mainfrom
jamesandersen:feat/required-fields-available-instruments

Conversation

@jamesandersen
Copy link
Copy Markdown
Contributor

@jamesandersen jamesandersen commented Mar 22, 2026

Enhancement Proposal: Card Payment Constraints

Summary

Adds named constraints to card_payment_instrument.json that 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.json but most merchants require it for card-not-present transactions. A handler like dev.shopify.card serves 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 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.

New Constraints

Constraint Type Description
requires_card_verification boolean When true, the handler requires card verification data. For FPAN: CVC. For network tokens: cryptogram and ECI.
requires_billing_address boolean When true, the handler requires a billing address on the instrument.
requires_billing_postal_code boolean When true, the handler requires a billing postal code for AVS verification. Ignored when requires_billing_address is true.

Example

{
  "available_instruments": [
    {
      "type": "card",
      "constraints": {
        "brands": ["visa", "mastercard"],
        "requires_card_verification": true,
        "requires_billing_postal_code": true
      }
    }
  ]
}

Per-card_number_type Semantics

The requires_card_verification constraint adapts to the credential mode:

card_number_type Verification fields required
fpan cvc
network_token cryptogram, eci_value

The spec documents these mappings. A future PR could make the schema fully self-documenting via a verification object with conditionals per card_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 — Added requires_card_verification, requires_billing_address, and requires_billing_postal_code to the available_card_payment_instrument constraints
  • docs/specification/payment-handler-guide.md — Added Card Constraints section with constraint table, example, and resolution flow documentation

Test Plan

  • JSON Schema validation: new constraints accepted on card_payment_instrument.json
  • Spell check (cspell): 0 issues
  • Markdown lint (markdownlint): 0 issues
  • Round-trip test: verify constraints flow through resolution in available_instruments

References

  • PR #187 — Original constraints mechanism
  • PR #49 — Prior instrument schema discussion

Type of change

  • New feature (non-breaking change which adds functionality)
  • Documentation update

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings

@google-cla
Copy link
Copy Markdown

google-cla bot commented Mar 22, 2026

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.

@jamesandersen jamesandersen force-pushed the feat/required-fields-available-instruments branch from 69a3cd0 to de0b8e6 Compare March 22, 2026 06:01
Copy link
Copy Markdown
Contributor

@alexpark20 alexpark20 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@igrigorik igrigorik added the TC review Ready for TC review label Mar 30, 2026
@jamesandersen
Copy link
Copy Markdown
Contributor Author

@alexpark20 thanks for taking time to do the review! The context on #49 is useful.

I agree that schema is likely where one would expect new defs specific for the payment handler to live and it might be non-obvious if that same schema URL also was trying to refine other payment instrument and credential types e.g. card_payment_instrument.json and card_credential.json e.g. . Using instrument_schema would help with the clarity there. As you noted, this does offer the full expression of JSON schema syntax to essentially say "for this handler, the card credential you can supply to the card payment instrument looks a bit different from the standard definition in that cvc is a required field"

While I think I could get behind this idea here's a couple additional thoughts for why the required_fields still feels a bit more appealing to me:

  • Requirements can vary per business, not per handler. A handler like dev.shopify.card serves thousands of merchants — some require CVC, some don't (it's a per-merchant fraud setting). required_fields lives on available_instruments, which already participates in the resolution flow, so it resolves per checkout naturally. A static instrument_schema URI can't vary per merchant without dynamically generating schema documents.
    • FWIW this is the exact problem we've encountered which motivated this PR ;-)
  • Low overhead for a narrow problem. The platform needs to answer "does this merchant require CVC?". required_fields keeps that answer inline in the discovery response with no additional fetch, no $ref resolution across a schema inheritance to refine types, no JSON Schema evaluation engine. We could indicate that the dot-notation syntax follows RFC 9535 (JSONPath).
  • Complementary, not competing. If the community wants instrument_schema for richer constraints down the road, required_fields wouldn't conflict.

To make it concrete — here's the Shopify card handler under each approach:

required_fields:

{
  "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"] 
  }]
}

instrument_schema — same declaration plus a new URI, and Shopify hosts a separate schema document:

// 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 required_fields approach. LMK what you think.

@jamesandersen jamesandersen force-pushed the feat/required-fields-available-instruments branch from de0b8e6 to ba16c3e Compare March 31, 2026 19:44
@alexpark20
Copy link
Copy Markdown
Contributor

@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 required_fields inline in available_instruments is the right place for per-merchant variance to provide that information at run time.

The one gap I see in required_fields alone is that the dot-notation strings are unvalidated (i.e typo such as credential.cvv). Nothing ties "credential.cvc" back to an actual property in the composed instrument and credential schema. I think having the static instrument schema (either via existing schema or new instrument_schema, but I lean into the latter) that platform can fetch / resolve during the discovery closes that gap. In addition, the spec doc can address it explicitly by stating something like

the business MUST only include paths in required_fields that correspond to valid properties defined in ucp schema and instrument_schema, and the platform SHOULD validate accordingly

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 required_fields. The schema defines the shape and the outer bounds. required_fields fills in the runtime specifics.


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.

@jamesandersen jamesandersen force-pushed the feat/required-fields-available-instruments branch from 01e3cae to 463631f Compare April 3, 2026 06:14
@jamesandersen jamesandersen changed the title feat: add required_fields to available payment instruments feat: add required_fields and instrument_schema to available payment instruments Apr 3, 2026
@jamesandersen
Copy link
Copy Markdown
Contributor Author

@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 required_fields and instrument_schema:

  • instrument_schema (new) — a URI to a JSON Schema defining the full instrument structure for the handler. Gives platforms a static, structural definition to validate required_fields paths against. No new operational overhead since platforms already fetch/evaluate handler schemas during discovery.
  • required_fields (existing) — runtime per-merchant requirements, resolved per checkout via the existing available_instruments resolution flow.

I've also added normative language per your suggestion:

The business MUST only include paths in required_fields that correspond to valid properties defined in the applicable UCP instrument and credential schemas or the instrument_schema. The platform SHOULD validate accordingly.

The PR description and docs have been updated to reflect both properties and how they complement each other. Let me know what you think!

@raginpirate
Copy link
Copy Markdown
Contributor

@jamesandersen @alexpark20 -- great discussion here! ❤️ I want to bring my perspective on this.

instrument_schema: the handler schema already carries instrument schema refs. I am not sure I see the value of bringing this top-level, because from my POV I think the handler must be understood by the platform before they can do anything with the instrument, meaning the discovery path for the schemas should be reasonable as they are today. Let me know what I might have failed to understand from your perspectives above 🤔

required_fields: payment_instrument.constraints already supports this power I believe. An example being, someone could add "requires_billing_address": boolean as a constraint on the card instrument to enforce it being provided, and they can do that today in their own handlers or extend that into the base card UCP schema as well to unify that with the protocol. Same for requires_cvv; but this one bends our abstraction a bit, let me expand.

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 requires_cvv is likely independent of the tokenization strategy; meaning you'd want to convey it at the instrument level even if conceptually the credential is hiding the cvv, because the credential scheme chosen is actually irrelevant even if the credential holds the cvv.

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?

@jamesandersen
Copy link
Copy Markdown
Contributor Author

jamesandersen commented Apr 6, 2026

@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 required_fields to instrument-level paths only (e.g., billing_address.postal_code) and using named constraints for cross-credential concerns like card verification. But that still requires constraints for the CVC case — which was the original motivation for this PR — and leaves you with two mechanisms to understand and implement.

I think the cleaner path is to standardize on constraints only: e.g. add new constraints fields like

  • requires_card_verification,
  • requires_billing_address, etc.

This gives us a single mechanism, consistent with #187. The tradeoff is that constraint semantics become implicit per credential scheme — requires_card_verification means "provide CVC" for raw cards but "provide cryptogram" for network tokens — so the spec needs to document those mappings. But one mechanism with clear documentation feels preferable to two mechanisms.

If this direction works for you and @alexpark20, I'll update the PR accordingly.

@kmcduffie
Copy link
Copy Markdown

@jamesandersen Based on your comment, I was reviewing the payment_instrument.json. The payment instrument contains a credential reference, so while they are separate structures, there's a hierarchy to the current modeling. Does this linkage help with them being separate object definitions? Can you help me understand the use case you're considering where the separate objects is causing a problem?

@jamesandersen
Copy link
Copy Markdown
Contributor Author

@kmcduffie good question — you're right that payment_instrument references credential, so the hierarchy exists. The challenge is that card_credential models both raw cards and network tokens in a single type via the card_number_type discriminator (fpan | network_token | dpan). The fields that matter differ depending on which mode is in play:

  • card_number_type: "fpan" → merchant may require cvc
  • card_number_type: "network_token"cryptogram and eci_value are relevant, but cvc is not

So the hierarchy tells the platform that a credential is attached, but since it's the same card_credential type either way, it can't distinguish which fields are actually required for a given checkout. A flat required_fields list like ["credential.cvc"] would incorrectly demand CVC even when the buyer is paying with a network token.

Full disclosure — this unified modeling initially threw me as well. In PR #296 I proposed a separate card_network_token_credential type, which would have naturally sidestepped this problem (distinct types = distinct requirements). But as @raginpirate pointed out there, UCP already models both modes in the unified card_credential — consistent with how many processor APIs handle it. Given that direction, the requirements-per-mode problem needs to be solved elsewhere, which is what led me toward named constraints like requires_card_verification: true. The spec would define what that means per card_number_type — "provide cvc" for fpan, "provide cryptogram/eci_value" for network_token — without the merchant needing to enumerate credential-level paths.

Quick illustration:

{
  "available_instruments": [{
    "type": "card",
    "constraints": {
      "brands": ["visa", "mastercard"],
      "requires_card_verification": true
    }
  }]
}

The platform sees requires_card_verification and knows from the spec to collect cvc for an FPAN or cryptogram/eci_value for a network token — one mechanism, consistent with #187.

@alexpark20
Copy link
Copy Markdown
Contributor

alexpark20 commented Apr 10, 2026

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 constraints already has the power to express what required_fields is trying to do, and I agree we should use a single mechanism rather than introducing a parallel one 👍

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.

  1. Instrument-credential binding is loose

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 requires_card_verification), this loose binding becomes a real gap. There is nothing in the schema that formally ties which credential type applies to which instrument type.

As you mentioned already @raginpirate , should we consider introducing available_credentials to formalize this binding? Now that we have a concrete use case (credential-level requirements expressed at the instrument level) ? Or is there other nuances I might missed?

  1. card_credential.json

Right now cvc, cryptogram, and eci_value are flat siblings on card_credential. The meaning of "card verification" is entirely implicit per card_number_type. I think we could make this more explicit by grouping them into a verification object (naming could be different) with conditionals that enforce which fields are required per mode:

"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, requires_card_verification: true as a named constraint would have a clear and enforceable meaning. The verification object is required, and the schema handles which subfields are needed per mode. We can avoid relying on spec documentation to map implicit semantics.


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 instrument_schema would serve a different purpose, which is building/verifying the instrument payload submitted. These are related but technically separated entities. Is the benefit of keeping both in the same schema simplicity? Convenience? Just want to understand the reasoning.

@jamesandersen
Copy link
Copy Markdown
Contributor Author

Thanks @alexpark20 — the verification object proposal is a clean way to make the schema self-documenting per card_number_type. I think that's a valuable direction, though getting the modeling right there will take some more involved alignment across the group.

Would folks be open to landing this PR as a pragmatic step forward? Concretely, that would mean adding fields like requires_card_verification and requires_billing_address to payment_instrument.constraints, with spec language documenting the per-card_number_type semantics (CVC for FPAN, cryptogram/ECI for network tokens). This gives platforms a way to understand per-merchant requirements today, with fully self-documenting schema (the verification object, available_credentials formalization) as a potential follow-up PR.

I'd update this PR to drop required_fields and instrument_schema, and add the named constraints instead. @raginpirate @alexpark20 does that work?

@raginpirate
Copy link
Copy Markdown
Contributor

raginpirate commented Apr 14, 2026

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 🤿

An instrument_schema would serve a different purpose, which is building/verifying the instrument payload submitted. These are related but technically separated entities. Is the benefit of keeping both in the same schema simplicity? Convenience? Just want to understand the reasoning.

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.

As you mentioned already @raginpirate , should we consider introducing available_credentials to formalize this binding? Now that we have a concrete use case (credential-level requirements expressed at the instrument level) ? Or is there other nuances I might missed?

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!

This way, requires_card_verification: true as a named constraint would have a clear and enforceable meaning. The verification object is required, and the schema handles which subfields are needed per mode. We can avoid relying on spec documentation to map implicit semantics.

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.
@jamesandersen jamesandersen force-pushed the feat/required-fields-available-instruments branch from c543d1c to 22b4708 Compare April 14, 2026 22:01
@jamesandersen jamesandersen changed the title feat: add required_fields and instrument_schema to available payment instruments feat: add card verification and billing address constraints Apr 14, 2026
@jamesandersen
Copy link
Copy Markdown
Contributor Author

PR updated — dropped required_fields and instrument_schema, replaced with three named constraints on card_payment_instrument.json:

  • requires_card_verification
  • requires_billing_address
  • requires_billing_postal_code

Rebased on latest main, docs updated. Awaiting any further review or, if aligned, approval to merge. @raginpirate @alexpark20

@alexpark20
Copy link
Copy Markdown
Contributor

Thanks for the updates @jamesandersen, and @raginpirate for the thoughts on my earlier point 👏

I am also aligned with landing this as named constraints on card_payment_instrument.json, which is consistent with #187. We can keep this PR tight.


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, requires_card_verification still carries implicit per type semantics (CVC for FPAN, cryptogram for network tokens) that the spec has to document and implementors have to know. Grouping under a verification object with conditionals would make the schema self-enforcing rather than relying on that documentation.

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.

Comment thread docs/specification/payment-handler-guide.md Outdated
Comment thread docs/specification/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
Copy link
Copy Markdown
Contributor

@alexpark20 alexpark20 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for iterating and addressing all the feedbacks 👏

Copy link
Copy Markdown
Contributor

@raginpirate raginpirate left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +33 to 42
"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."
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this better to represent as an address constraint with three example values (full_address, postal_code, nil)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

payments TC review Ready for TC review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants