Skip to content

feat: structured constraints for instruments and beyond#424

Draft
raginpirate wants to merge 1 commit into
mainfrom
proto/funding-source-and-credential-constraints
Draft

feat: structured constraints for instruments and beyond#424
raginpirate wants to merge 1 commit into
mainfrom
proto/funding-source-and-credential-constraints

Conversation

@raginpirate
Copy link
Copy Markdown
Contributor

@raginpirate raginpirate commented May 8, 2026

Prototype revision related to #288
This is heavily AI generated to quickly show the thinking and primitives; do not trust the descriptions or small implementation quirks here!

What

Introduces a single constraint primitive (constraint.json) and applies it consistently across instrument and credential availability. Replaces ad-hoc booleans (requires_card_verification,
billing_address_granularity) with a uniform shape: required_fields[] plus domain-specific custom keys.

New primitives

  • constraint.json — universal base: required_fields[] of property names + open additionalProperties for domain keys. Every constraint object in UCP follows this shape.
  • payment_funding_source.json — intermediate credential schema. Concrete funding-source credentials (card_credential, network_token_credential) inherit from it; consumers detect funding sources by ancestry, not
    annotation.
  • address_constraint.json — constraint-shaped, required_fields scoped to postal_address.json properties. Reusable across payments and fulfillment.
  • credential_constraint.json — { type, constraints } entry. Single primitive used on both axes below.

Reshaped

  • Card credentials split by verification scheme, not by PAN flavor.
    • card_credential.json carries cvc. Used whenever the verification is a CVC, regardless of whether number is an FPAN, network token, or wallet token.
    • network_token_credential.json (new) carries cryptogram + eci_value as schema-required. Used whenever the verification is cryptographic.
    • The card_number_type discriminator goes away — verification fields define the type.
  • available_card_payment_instrument.constraints exposes four top-level keys:
    • required_fields — names instrument properties that MUST be present (e.g. billing_address).
    • brands — accepted card brands (instrument-level; brand isn't a credential field).
    • billing_address — address_constraint describing what address fields must be populated.
    • credentials[] / funding_sources[] — same credential_constraint primitive on two orthogonal axes. credentials constrains the wire shape the handler ingests; funding_sources constrains the underlying instrument
      behind the wire credential. Constraints sit on the semantically correct axis only — never duplicated.

Cross-domain reuse

fulfillment_available_method.json adopts the same primitives — top-level constraints extending constraint.json, with destination reusing address_constraint.json. Closes the address-constraint gap on the
fulfillment side without inventing parallel concepts.

Example

  {
    "type": "card",
    "constraints": {
      "required_fields": ["billing_address"],
      "brands": ["visa", "mastercard"],
      "billing_address": {
        "required_fields": ["postal_code", "address_country"]
      },
      "credentials": [
        { "type": "token", "constraints": { "token_provider": ["dev.shopify.spt"] } }
      ],
      "funding_sources": [
        { "type": "card",          "constraints": { "required_fields": ["cvc"] } },
        { "type": "network_token" }
      ]
    }
  }

Reads as: handler accepts a token credential on the wire, conveying either a CVC-verified card or a cryptogram-verified network token. Merchant requires CVC on raw cards, requires a billing address with at least
postal code + country, and accepts visa/mastercard.

Copy link
Copy Markdown
Contributor

@jamesandersen jamesandersen left a comment

Choose a reason for hiding this comment

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

@raginpirate thanks for putting #424 together — I do think this proposal works well:

  • (+) Introduces a new credential type network_token
    • Adds the distinction I was looking for originally in #296 ;-)
    • ... and probably will have implications for the doc changes proposed in #367
  • (+) Required fields are explicit via the new constraint primitive
    • e.g. under instrument -> credentials -> constraints -> required_fields (more like where #288 started but now implemented at the right level)
    • In contrast to where #288 was trending with implicit constraints via instrument -> constraints -> requires_card_verification and docs to explain intent at the credential level
  • (+) Cross-domain reuse e.g. address_constraint.json, credential_constraint.json
  • (-) Breaking change with removal of card_number_type ... left separate comment on this

On the "credential inception" point e.g. distinct constraint on a wire credential and - where applicable - the funding source underlying it. What do you think about taking this on in a separate PR? I'd like to better understand if there is a concrete scenario requiring both? e.g. can a business require a CVV on the underlying instrument while only accepting the network token? It's been a winding road so far to get alignment on updated constraint modeling for just the "wire credential" ... hopefully a follow-on for the underlying funding source could be quicker and cleaner after landing this.

TBH ... it took a while to grok this all (in large part for lack of good focus time) but I think the resolved examples are actually much easier than the schema suggests at first glance - just as another sanity check the multiple entries under credentials is how we address the hurdle that led us to the implicit bool and AVS enum on #288. Look right?

  {                                                                                                                            
    "available_instruments": [                              
      {                                                                                                                        
        "type": "card",  // card-based instrument                                                                                         
        "constraints": {                                                                               
          "brands": ["visa", "mastercard"],                                                                                    
          "required_fields": ["billing_address"],                                                                     
          "billing_address": {                                                                                                 
            "required_fields": ["postal_code"] // e.g. AVS1, but adapts to other schemes                                                                    
          },                                                                                                                   
          "credentials": [
            { 
              "type": "card", // FPAN
              "constraints": { "required_fields": ["cvc"] } 
            },
            { 
              "type": "network_token", // DPAN / CPAN
              "constraints": { "required_fields": ["cryptogram"] } // could add ECI if a business needs it
            }
          ]                                                                                                                    
        }                                                   
      },                                                                                                                        
    ]                                                                                                                          
  }

"const": "card",
"description": "The credential type identifier for card credentials."
},
"card_number_type": {
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.

Should we deprecate rather than removing to avoid a breaking change?

},
"cryptogram": {
"type": "string",
"description": "Per-transaction cryptogram. Structurally required — a network token credential without a cryptogram is a CVC-verified card; submit it as `card_credential` instead."
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.

nit: Could we indicate this may be either long form or short form (e.g. dCVV)

{
"type": "object",
"required": ["type", "number", "cryptogram", "eci_value"],
"properties": {
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.

Thoughts about adding token_requestor_id (#296) onto network_token_credential while we're at it? not widely used as discussed on that PR but we do have the Braintree BYOT example

@@ -0,0 +1,61 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/shopping/types/network_token_credential.json",
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.

This scratches that itch I had from #296 ;-)

@TateLyman
Copy link
Copy Markdown

Schema read only.

One validation gap to keep an eye on if this moves from prototype to spec shape: credential_constraint.json describes constraints as being scoped to the named credential type, and the concrete credential schemas define narrowed $defs.constraint objects, but the actual reference is still the generic base constraint:

"constraints": {
  "$ref": "constraint.json"
}

Because of that, JSON Schema validation will not use type to select card_credential.json#/$defs/constraint or network_token_credential.json#/$defs/constraint. For example, these look invalid by the intended semantics but appear valid against the current primitive:

{ "type": "card", "constraints": { "required_fields": ["cryptogram"] } }
{ "type": "network_token", "constraints": { "required_fields": ["cvc"] } }

Same issue for funding_sources: the body says entries must reference schemas inheriting from payment_funding_source.json, but type is an unconstrained string in the schema itself.

Suggested direction: either make credential_constraint an explicit oneOf over known credential types (if type == card then constraints -> card_credential#/$defs/constraint, etc.), or keep it intentionally registry/runtime-validated and soften the schema language so platforms do not assume this is machine-checkable from the JSON Schema alone.

The interoperability risk is that merchants/platforms may think constraint payloads are statically validated, while cross-axis mistakes only exist in prose. That matters for checkout because a mistyped funding-source requirement can become a failed tokenization/payment path rather than a schema error.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants