Skip to content

feat: structured constraints for instruments and beyond - #424

Open
raginpirate wants to merge 4 commits into
mainfrom
proto/funding-source-and-credential-constraints
Open

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

Conversation

@raginpirate

@raginpirate raginpirate commented May 8, 2026

Copy link
Copy Markdown
Member

Larger-scope continuation of #288.

Follow-up applications are stacked in #564 (WIP) and #565 (WIP). The final shape also incorporates lessons from the alternative explored in #580.

Introduces a small, composable constraint vocabulary for advertising runtime requirements without changing a target's static schema or adding domain-specific booleans such as requires_card_verification.

New primitives

  • object_constraint.json — applies to an object. required[] names properties that must be present; concrete schemas add nested constraints and domain-specific keys.
  • value_constraint.json — applies to a property's value using a bounded JSON Schema vocabulary, initially enum and const.
  • type_constraint.json — selects a branch of a typed family with { type, constraints }, where constraints is an Object Constraint.

Object and Type Constraints remain open so handlers and extensions can compose their own keys and types. Value Constraints standardize common value restrictions. Literal domain constraints remain defined and enforced by their owning specification.

Payment-handler application

available_instruments[] is modeled as an array of Type Constraints:

  • type selects an instrument branch from the negotiated handler schema.
  • constraints applies an Object Constraint to acceptable instruments of that type.
  • billing_address applies a nested Object Constraint.
  • credentials[] applies Type Constraints to accepted credential branches.
  • Concrete instrument schemas add their own keys, such as card brands.

The generic Payment Handler remains open and does not globally dispatch instrument type strings. Concrete availability shapes are supplied by the negotiated handler's available_payment_instrument definition.

The schema-authoring and payment-handler guides document composition, extension, and runtime typing expectations.

Category (Required)

Please select one or more categories that apply to this change.

  • Core Protocol: Changes to the base communication layer, global context, or breaking refactors. (Requires Technical Council approval)

Checklist

  • I have followed the Contributing Guide (including Conventional Commits title requirements and ! for breaking changes).
  • I have updated the documentation (if applicable).
  • My changes pass all local linting and formatting checks.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • (For Core/Capability) I have included/updated the relevant JSON schemas.
  • I have regenerated Python Pydantic models by running generate_models.sh under python_sdk.

@jamesandersen jamesandersen left a comment

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.

@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
            }
          ]                                                                                                                    
        }                                                   
      },                                                                                                                        
    ]                                                                                                                          
  }

Comment thread source/schemas/shopping/types/card_credential.json
Comment thread source/schemas/shopping/types/network_token_credential.json Outdated
{
"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

Comment thread source/schemas/shopping/types/network_token_credential.json Outdated
@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.

@jamesandersen

jamesandersen commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Great catch @TateLyman — this is a real validation gap. As you noted, the type discriminator in credential_constraint.json doesn't drive schema selection for the constraints body, so cross-axis mistakes like { "type": "card", "constraints": { "required_fields": ["cryptogram"] } } pass validation silently and only surface as payment failures. This matters particularly because UCP aims for a machine-verifiable schema — it's the foundation for SDK generation, so constraints that are only enforced in prose undermine that goal.

Your if/then suggestion would work well and has precedent in UCP — total.json already uses this exact pattern (discriminating on type to apply different validation rules to a sibling property), as does pagination.json (conditional field requirements based on a sibling value).

A couple of alternative approaches that achieve the same schema validation, for consideration:

Per-credential $defs with oneOf dispatch: Each credential schema (card_credential.json, network_token_credential.json) already defines a $defs/constraint narrowing required_fields. Each could additionally define a $defs/credential_constraint that bundles type: { const: "card" } with a constraints ref to its own $defs/constraint. The instrument schema would then use oneOf over those refs instead of referencing credential_constraint.json directly. This keeps each credential schema self-contained — no central file needs to know about all types.

Single $defs/constraint (breaking change): Take the above a step further — instead of adding a parallel $defs/credential_constraint, redefine $defs/constraint itself to be the full credential constraint entry (extending credential_constraint.json via allOf, adding the type const and narrowed constraints). One concept, one $defs entry per credential schema. Cleanest mental model, but this would be a breaking change to the main schema if merged. Worth weighing whether the value of a tighter, self-contained schema outweighs the inconvenience to implementations that have already adopted $defs/constraint in its current form.

All three approaches reject the same invalid payloads — the difference is where the enforcement lives: central dispatch (if/then), per-schema with two defs, or per-schema with one def.

@raginpirate WDYT?

@raginpirate
raginpirate force-pushed the proto/funding-source-and-credential-constraints branch from 1fa04e8 to 6b04550 Compare June 8, 2026 06:06
@raginpirate

raginpirate commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

Thanks for the comments folks; totally agree with the schema validation concern. This actually goes back to the original payment handler design; we did not associate instruments with any credential types as an attempt to show they are open objects to be associated as needed. In that same vein, I did not originally try having machine-enforcable types for the constraints, but honestly I think this is just a gap we can close. In my latest revision I've actually taken the oneOf approach for both constraints as well as credentials themselves 👍

I wanted to get this into review this weekend but it took quite a bit of time shredding my original proposal apart here: one of the main contentious points is "funding sources" as you pointed out @jamesandersen, and in trying to isolate it I reverted my world view on the concept and moved it into a type of constraint specific to token credentials.

I'm feeling good about the work in this PR, and my steps to get this ready to publish are:

  • Isolate each commit into its own PR
  • Add documentation for schema-authoring to describe how to write constraints across UCP
  • Rewrite documentation for payment handlers across the board to include our new constraints

One interesting note is still how we refactor the card credential: I think the right path is to actually entirely deprecate it and just introduce a PAN credential and a network token credential. Its technically non-breaking too because card is isolated and attached to nothing in the base spec; handlers in the wild using it can just keep referencing it from the old api versions without concern if they really want to. WDYT about this or the plan above?

@jamesandersen

Copy link
Copy Markdown
Contributor

"funding sources" as you pointed out @jamesandersen, and in trying to isolate it I reverted my world view on the concept and moved it into a type of constraint specific to token credentials.

@raginpirate makes sense to me ... if introducing distinct PRs I think this will become more digestible ;-)

deprecate it and just introduce a PAN credential and a network token credential

I do think this is going to be easier to reason about in the long run

@kmcduffie
kmcduffie requested a review from GarethCOliver June 16, 2026 14:12
@raginpirate
raginpirate force-pushed the proto/funding-source-and-credential-constraints branch from 6b04550 to 2371672 Compare July 6, 2026 05:33
@raginpirate
raginpirate requested a review from jamesandersen July 6, 2026 05:43
@raginpirate
raginpirate marked this pull request as ready for review July 6, 2026 05:43
@raginpirate
raginpirate requested review from a team as code owners July 6, 2026 05:43
@raginpirate raginpirate self-assigned this Jul 6, 2026
@raginpirate raginpirate added TC review Ready for TC review and removed WIP labels Jul 6, 2026
@raginpirate
raginpirate force-pushed the proto/funding-source-and-credential-constraints branch from 2371672 to 446e45f Compare July 6, 2026 05:47
@igrigorik igrigorik added this to the Working Draft milestone Jul 6, 2026

@igrigorik igrigorik left a comment

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.

Catching up, great discussion. The direction makes sense, I agree that modelling this as booleans doesn't scale and pollutes the schema. Reading the current shape, though, I hit a number of gotchas and I'm wondering if we're over-promising here.

Meta issue: constraints isn't actually generic

The pitch is a universal primitive ("every constraint object in UCP follows this shape"), but it isn't one — it's a shape you have to graft onto every object that negotiates input. That's a mixin re-attached per-object, not a generic contract, and it doesn't scale as the surface grows; we'd have to include this on every object at the limit.

#564 is the early evidence: it reuses the primitive as fulfillment_available_method.constraints.destination, but destination isn't a field of the fulfillment method, so it already breaks the base contract ("required_fields names properties of the constrained object").

If it is meant to be generic, it has to be extensibility-friendly

This rules out enums / closed dispatch. allOf can only intersect, never widen — so any closed construct in the base permanently excludes extensions:

  • Enum: address_constraint.required_fields is a closed enum of the 9 canonical fields. postal_address.json is open, so a handler can add tax_id — but can never require it ("required_fields": ["tax_id"] is rejected). A "declare what you need" primitive that can't name an extended field isn't extensibility-friendly.
  • Closed dispatch: validating a typed family by enumerating known types in the base rejects extension types. The credentials oneOf here avoids that only by adding an open catch-all branch + type_constraint to stay open.

And the validation this complexity buys isn't actually enforced. The brands and credentials rules live on the card schema, but a handler declaration validates against the base available_payment_instrument.json — which never dispatches on type == "card" to pull those rules in. So they go unchecked: the documented business_schema example still passes the repo's own validator even with brands broken (change ["visa","mastercard"] to a bare "visa" and it's still valid). Same gap @TateLyman/@jamesandersen flagged for credentials, one level up.


Stepping back: are we over-generalizing this problem?

Reactive feedback is the established mechanism, and it already handles dynamic requirements. Submit what you have → checkout.status: "incomplete" + messages[] with message_error.path (RFC 9535 JSONPath) and severity: "recoverable" ("fix inputs and retry via API"). Zero new schema, works with extensions and responds to dynamic requirements of the current negotiation. This does put an obligation on handlers to validate and return messages before tokenizing (rather than failing at the PSP) — but that's a smaller, more local ask than a new schema primitive. The main gap is that it costs a roundtrip.

I'm not against modeling some pre-submit requirements. An upfront signal is genuinely useful, so a platform can collect complete input (and render the right fields per option) before the first submit. Also agree that bools were the wrong mechanism.

Alt, what if: model it as requires array of JSONPaths...

{
  "type": "card",
  "accepts": {
    "brands": ["visa", "mastercard"],
    "credentials": [
      { "type": "card",          
         "requires": ["$.payment.instrument.credential.cvc"] },
      { "type": "network_token", 
         "requires": ["$.payment.instrument.credential.cryptogram"] }
    ]
  },
  "requires": ["$.payment.instrument.billing_address.postal_code"]
}
  • One vocabulary across requires and message_error.path
  • requires is a minimum, not a completeness guarantee
  • Models arbitrary requirements without introducing N booleans
  • Applied to payments, can be selectively adopted in other places if/as necessary

Net: no constraint.json / type_constraint.json / address_constraint.json, no enum, no per-object algebra, requirements separated from capabilities, and nothing to misapply (so #564 doesn't arise). This pattern can be applied selectively to other places too, but we would use it surgically in select places, instead of promising a generic contract that we can't deliver.

@jamesandersen

jamesandersen commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Appreciate this @igrigorik — good callout that even the landed brands constraints aren't really being enforced today (a handler config validates against the base available_payment_instrument.json, which never dispatches on type == "card", so the card-specific brands rule is never applied — a malformed brands passes validation). That's a fair reason to rethink the shape rather than build more on top of it.

On the JSONPath direction: worth naming that its main weakness — unvalidated paths (a cvv/cvc typo passes silently) — is the same one @alexpark20 raised on #288 that originally pushed us toward a schema-based approach. That said, you've pointed out that the schema/$defs route can get cumbersome quickly (constraints applied at the base getting out of sync with future object extensions) and still not be fully functional, I'm supportive of consciously going with requires: [JSONPath] — the consistency with existing UCP mechanisms (shared vocabulary with message_error.path) is worth more here than validation we're not actually delivering.

The one thing I'd hold firm on is the need for an upfront signal. The anchoring use case from #288 is a platform knowing before it submits complete checkout whether a business' payment handler requires CVV (a per-merchant setting on handlers like dev.shopify.card) — otherwise the platform guesses or eats a failed tokenization. Your requires proposal would solve that.

@raginpirate looks like this would still build on the distinct card / network_token credential types introduced here which I'm a fan of. WDYT?

@igrigorik

Copy link
Copy Markdown
Contributor

@jamesandersen — agreed on both, and I think we can close the typo concern rather than eat it...

Upfront signal: yes, requires[] is exactly that — declared at discovery so the platform knows before it submits.

Credential types: requires is type-agnostic — it co-locates on each credential entry, so it rides on card/network_token (or whatever we land).

On the typo gap: rather than an enum on the wire (which can't name extension fields), we can teach ucp-schema to resolve each requires path against the composed instrument/credential schema — a path to a non-existent property fails. That's build-time/CI validation (whoever authors the path runs it, same as any UCP payload), not a forced per-handshake operation, which I think is a nice balance.

@raginpirate
raginpirate force-pushed the proto/funding-source-and-credential-constraints branch from 446e45f to 9495857 Compare July 10, 2026 06:08
@raginpirate

raginpirate commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

Thanks for the feedback @igrigorik and @jamesandersen! ❤️

  • Totally right call to avoid enums, that was an overstep on my part where I looked at it from an "unlikely to change" perspective rather than an immutability perspective. Made the change on this to kill the address constraint and simplify the base constraint language. With this, I don't see any other extensibility concerns in the PR, but please flag if there still is.
  • Also aligned on the typing concern: I recognize that no matter what, once we shift away from being able to use an enum on these branches, the type enforcement level I tried pursuing was off-base. Also made the changes on this front to just keep the typing simple across the board.

Now... hear me out, I am not sold on the shift from constraints as a mixin to this requires proposal. When I look at anywhere I'd want to place this requires field, I notice that I'm actually colocating it within an object that uses convention to accomplish the path your encoding there... examples being:

  • available_payment_instrument already implicitly targets the payment instrument
  • fulfillment_available_method already implicitly targets the fulfillment method
  • the config of an extension
    I think that having the fullpath on messages is important because the entirety of the message is bound to a single target point: but these objects are constraining a wide number of fields and subfields.

Maybe to help drive it home, the $path based proposal:

  {                                                                                                                                                       
    "available_instruments": [                                                                                                                            
      {                                                                                                                                                   
        "type": "card",                                                                                                                                   
        "constraints": {                                                                                                                                      
          "brands": ["visa", "mastercard"],                                                                                                               
          "credentials": [                                                                                                                                
            {                                                                                                                                             
              "type": "pan",                                                                                                                              
              "requires": [                                                                                                                               
                "$.payment.instruments[].credential.cvc"                                                                             
              ]                                                                                                                                           
            },                                                                                                                                            
            {                                                                                                                                             
              "type": "network_token",                                                                                                                    
              "requires": [                                                                                                                               
                "$.payment.instruments[].credential.cryptogram"                                                                      
              ]                                                                                                                                           
            }                                                                                                                                             
          ],                                                                                                                                                
        "requires": [                                                                                                                                     
          "$.payment.instruments[].billing_address.postal_code",                                                                     
          "$.payment.instruments[].billing_address.address_country"                                                                  
        ]                                                                                                                                                
        }                                                                                                                                                
      }                                                                                                                                                   
    ]                                                                                                                                                     
  }                                                                                                                                                       

The localized required_fields proposal:

{                                                                                                                                                       
  "available_instruments": [                                                                                                                            
    {                                                                                                                                                   
      "type": "card",                                                                                                                                   
      "constraints": {                                                                                                                                  
        "brands": ["visa", "mastercard"],                                                                                                               
        "required_fields": ["billing_address"],                                                                                                         
        "billing_address": {                                                                                                                            
          "required_fields": ["postal_code", "address_country"]                                                                                         
        },                                                                                                                                              
        "credentials": [                                                                                                                                
          {                                                                                                                                             
            "type": "pan",                                                                                                                              
            "constraints": {                                                                                                                            
              "required_fields": ["cvc"]                                                                                                                
            }                                                                                                                                           
          },                                                                                                                                            
          {                                                                                                                                             
            "type": "network_token",                                                                                                                    
            "constraints": {                                                                                                                            
              "required_fields": ["cryptogram"]                                                                                                         
            }                                                                                                                                           
          }                                                                                                                                             
        ]                                                                                                                                               
      }                                                                                                                                                 
    }                                                                                                                                                   
  ]                                                                                                                                                     
}                                                                                                                                                       

Very similar but I think the later is significantly less noisy, and your earlier feedback greatly improved the complexity and correctness of the final solution.

WDYT? I'm happy to be pushed back on if theres a benefit I've missed on the fullpath here. Also happy to rename required_fields to requires if you think thats a better name!

@igrigorik

Copy link
Copy Markdown
Contributor

@raginpirate good turn, I think we're converging on right shape. As a wildcard thought experiment, what if we push this one step further: #580

@jamesandersen

Copy link
Copy Markdown
Contributor

Really like where this landed, @raginpirate — dropping address_constraint.json and the closed enum in favor of the open required_fields: string[] addresses the extensibility concern cleanly, and the if type == "token" then … dispatch on credentials is exactly the right shape: it validates known credential types while letting handler-extended types fall through to the open base, no rejection. That's additive and extension-safe.

Good call on keeping local required_fields over the requires: [JSONPath] direction — a constraint object already implicitly targets its host, so full paths would be redundant here. Agreed.

The one item I don't think is closed yet is the enforcement gap:

  • payment_handler.json → available_instruments[].items refs the base available_payment_instrument.json.
  • The base doesn't dispatch on type, so the card-specific rules (brands, the if type==token credential dispatch) — which live in card_payment_instrument.json#/$defs/available_card_payment_instrument — are never pulled into the validated path.
  • Concretely: a handler declaring { "type": "card", "constraints": { "brands": "visa" } } (bare string, malformed) still validates clean, because it only ever hits the base's open additionalProperties.

That's the same "validation isn't actually enforced" point from earlier, still true on HEAD. The fix is the same if/then pattern you already used for token credentials, just applied one level up at the instrument type — but it needs to sit at the point-of-use (or a small dispatcher schema), not in the base itself, since available_card_payment_instrument already allOfs the base and a base→card ref would recurse. Roughly:

// what available_instruments[].items validates against — a dispatcher, not the bare base
{
  "if":   { "properties": { "type": { "const": "card" } }, "required": ["type"] },
  "then": { "$ref": "card_payment_instrument.json#/$defs/available_card_payment_instrument" }
  // unknown instrument types fall through to the open base — extension-safe, same as your credential dispatch
}

That turns the brands/credential rules from documented-but-dormant into actually-enforced, and closes the gap without reintroducing enums or a base-level mixin.

@raginpirate
raginpirate force-pushed the proto/funding-source-and-credential-constraints branch from ef15676 to 3bd8c24 Compare July 21, 2026 01:34
@raginpirate

Copy link
Copy Markdown
Member Author

👋 Sorry for the slow loop here, @igrigorik @jamesandersen this is good for re-review!

TL;DR: I kept #424’s local constraint shape and pulled over the JSON Schema vocabulary from #580 that fit cleanly: required, plus enum and const. The proposal now has an explicit composition of Object, Value, and Type Constraints, while handler-specific literal constraints remain open.

@jamesandersen, I agree with the behaviour you observed, but not the proposed validation layer. A core if type == "card" dispatcher works mechanically, but it globally binds instrument type strings and requires payment_handler.json to enumerate UCP instrument schemas. The generic handler should remain open; the negotiated handler’s available_payment_instrument definition should compose and validate its concrete branches only, not the entire base spec's definitions.

I also don’t think the typing gap should remain unresolved. Some constraint-aware ucp-schema tools could resolve the target and concrete constraint schemas, check required names, and validate every constraint value against its declared type. Custom constraints such as brands remain semantically owned by their defining handler. I put together a small draft demonstrating that approach: https://github.com/Universal-Commerce-Protocol/ucp-schema/compare/raginpirate/constraint-evaluation <-- not advocating that even moves to draft yet, but a quick AI generated validator covering both our constraint validations as well as validations of the submitted instrument using a produced json fragment.

@raginpirate
raginpirate requested a review from igrigorik July 21, 2026 02:16

@jamesandersen jamesandersen left a comment

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.

Stepping back to what #288 was originally about — letting a handler say up front what it needs for an instrument so the agent can collect it before checkout — I think this version covers it. I took three practical use cases we'd want these changes to express and worked them through the current shape: requiring a CVV on card, requiring AVS fields from the billing address, and requiring a network-token cryptogram. All three express cleanly, and it's more flexible than #288's old requires_card_verification boolean. I've left them as a committable docs suggestion below so they're easy to fold into the guide.

On the base coupling: you're right that an if type == "card" dispatcher doesn't belong in the base. It would make the generic handler enumerate instrument schemas, which breaks the openness we wanted to keep. Dropping that.

On the enforcement gap: it's only about catching malformed constraints, not the well-formed ones expressing these use cases. Something like { "type": "card", "constraints": { "brands": "visa" } } (string instead of array) still validates when it shouldn't, but the valid declarations above work regardless.

@igrigorik — WDYT?

The base Payment Handler intentionally remains open: an instrument `type` does
not globally select a schema. After negotiation, consumers use
`$defs.{handler_name}.available_payment_instrument` from the handler schema.
Handlers that add no availability-specific keys can omit this definition.

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.

A few concrete examples would help here — this is how the three use cases map onto the primitives. Suggested insertion:

Suggested change
Handlers that add no availability-specific keys can omit this definition.
Handlers that add no availability-specific keys can omit this definition.
**Common constraint examples**
The same three primitives express the requirements handlers most often advertise
upfront. Each is an entry a handler publishes under `available_instruments[]`.
Require a card verification value (CVV) for card payments:
<!-- ucp:example skip reason="instrument availability instance" -->
```json
{
"type": "card",
"constraints": {
"credentials": [
{ "type": "card", "constraints": { "required": ["cvc"] } }
]
}
}
```
Require Address Verification System (AVS) fields from the billing address:
<!-- ucp:example skip reason="instrument availability instance" -->
```json
{
"type": "card",
"constraints": {
"billing_address": { "required": ["street_address", "postal_code"] }
}
}
```
Require a network-token cryptogram (and ECI value) on card credentials:
<!-- ucp:example skip reason="instrument availability instance" -->
```json
{
"type": "card",
"constraints": {
"credentials": [
{
"type": "card",
"constraints": {
"card_number_type": { "const": "network_token" },
"required": ["cryptogram", "eci_value"]
}
}
]
}
}
```

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.

8 participants