Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 44 additions & 29 deletions docs/documentation/schema-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,55 +429,70 @@ runtime requirement attached to a concrete context, not a global schema change.
All new constraint objects **SHOULD** extend
[`constraint.json`](site:{{ ucp_version }}/schemas/shopping/types/constraint.json):

- `required_fields` names fields on the local constrained object that must be
present in an acceptable instance for this context.
- The schema property that embeds the constraint defines the target object. For
example, `available_instruments[].constraints` targets the payment instrument,
while `constraints.billing_address` targets that instrument's billing address.
- Domain-specific constraint keys remain sibling properties. For example, the
base payment instrument can require `billing_address` and add a nested
`billing_address` constraint describing which address fields are needed.
- Reusable base schemas **SHOULD NOT** close `required_fields` with enums when
the target object can be extended. Concrete resolved schemas MAY narrow
`required_fields` when they own the complete target vocabulary, while leaving
the object open to future domain-specific keys.
- `required` names properties of the constrained object that must be present in
an acceptable instance for this context.
- `properties` maps a property name to a nested `Constraint` applied to that
property (recursive), e.g. `billing_address` narrowing which address fields are
required. The schema property that embeds the constraint defines the target
object: `available_instruments[].constraints` targets the payment instrument,
and `properties.billing_address` targets that instrument's billing address.
- `enum` restricts a property's value to a per-merchant allowed set. This is a
runtime, per-party value restriction, not a base-schema enum — it does not
close the protocol vocabulary.
- A constraint is a sparse overlay on an already-typed base schema, so it carries
no `type` and stays open: it narrows named properties and never forbids unknown
ones (no `additionalProperties:false`, `oneOf`, or `if`/`then`).

`constraints` covers only field-level requirements over the *submitted* object.
Two related but distinct axes use their own keys, **not** `constraints`:
**accepted-value menus** for derived or selected attributes (e.g.
`accepts: { "brand": ["visa", "mastercard"] }` — a card's network is derived, not
a submitted field, so it is a menu, not a field constraint), and **discriminated
per-subtype requirements** (e.g. `credentials`, below). Keeping them separate is
what lets `constraints` stay a genuine bounded JSON Schema over the object's own
fields.

Prefer field-level constraints over ad-hoc booleans. For example, prefer
`required_fields: ["billing_address"]` plus a nested address constraint over
new booleans such as `requires_billing_address` or
`required: ["billing_address"]` plus `properties.billing_address.required`
over new booleans such as `requires_billing_address` or
`requires_billing_postal_code`.

<!-- ucp:example skip reason="schema authoring example" -->
```json
{
"type": "card",
"constraints": {
"brands": ["visa", "mastercard"],
"required_fields": ["billing_address"],
"billing_address": {
"required_fields": ["postal_code", "address_country"]
"required": ["billing_address"],
"properties": {
"billing_address": {
"required": ["postal_code", "address_country"]
}
}
}
},
"accepts": { "brand": ["visa", "mastercard"] },
"credentials": [
{ "type": "pan", "constraints": { "required": ["cvc"] } },
{ "type": "network_token", "constraints": { "required": ["cryptogram"] } }
]
}
```

For arrays of constraints over a typed family, extend
[`type_constraint.json`](site:{{ ucp_version }}/schemas/shopping/types/type_constraint.json). Each
known branch should publish a `$defs/constraint` entry with a `type` discriminator
and, when applicable, a narrowed `constraints` body. The parent schema can then
validate UCP-defined branches while still leaving an extension point for
handler-specific branches.
For arrays of constraints over a typed family, use
[`type_constraint.json`](site:{{ ucp_version }}/schemas/shopping/types/type_constraint.json):
a list where each entry is a `Constraint` carrying a `type` discriminator plus
that branch's `required` fields. Discrimination is a data lookup over the list —
the consumer applies the entry matching the submitted instance's `type` — so
UCP-defined branches and handler/extension branches coexist without a schema
`oneOf` or `if`/`then`.

<!-- ucp:example skip reason="schema authoring example" -->
```json
{
"credentials": [
{ "type": "token" },
{ "type": "pan", "constraints": { "required": ["cvc"] } },
{
"type": "com.example.wallet_token",
"constraints": {
"required_fields": ["assurance_level"]
}
"constraints": { "required": ["assurance_level"] }
}
]
}
Expand Down
96 changes: 47 additions & 49 deletions docs/specification/payment-handler-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,8 @@ and typically includes different configuration:
"available_instruments": [
{
"type": "card",
"constraints": {
"brands": ["visa", "mastercard"]
"accepts": {
"brand": ["visa", "mastercard"]
}
}
],
Expand All @@ -242,8 +242,8 @@ and typically includes different configuration:
"available_instruments": [
{
"type": "card",
"constraints": {
"brands": ["visa", "mastercard", "amex", "discover"]
"accepts": {
"brand": ["visa", "mastercard", "amex", "discover"]
}
}
],
Expand All @@ -264,8 +264,8 @@ and typically includes different configuration:
"available_instruments": [
{
"type": "card",
"constraints": {
"brands": ["visa", "mastercard"]
"accepts": {
"brand": ["visa", "mastercard"]
}
}
],
Expand Down Expand Up @@ -305,43 +305,37 @@ authoritative value returned in the `response_schema`.

| Source | `available_instruments` |
| :----- | :---------------------- |
| Platform profile | `[{type: "card", constraints: {brands: ["visa", "mastercard", "amex", "discover"]}}]` |
| Business profile | `[{type: "card", constraints: {brands: ["visa", "mastercard", "amex"]}}]` |
| **Response (resolved)** | `[{type: "card", constraints: {brands: ["visa", "mastercard", "amex"]}}]` |
| Platform profile | `[{type: "card", accepts: {brand: ["visa", "mastercard", "amex", "discover"]}}]` |
| Business profile | `[{type: "card", accepts: {brand: ["visa", "mastercard", "amex"]}}]` |
| **Response (resolved)** | `[{type: "card", accepts: {brand: ["visa", "mastercard", "amex"]}}]` |

In this example, the business's PSP is not configured for Discover, so Discover
is excluded from the response even though the platform supports it.

#### Constraint Semantics

`available_instruments[].constraints` describes what an acceptable instrument
must satisfy for a handler declaration or resolved checkout response. Constraint
objects extend [`Constraint`](site:schemas/shopping/types/constraint.json):

| Constraint key | Meaning |
| :------------- | :------ |
| `required_fields` | Field names from the constrained object that must be present in this context. |
| Domain-specific keys | Additional constraints defined by the concrete instrument or handler schema. |
An `available_instruments[]` entry declares what an acceptable instrument must
satisfy along **three distinct axes**, each with its own mechanism:

Base payment instruments define these common constraints:

| Key | Description |
| :-- | :---------- |
| `required_fields` | Payment instrument fields required by this handler. The base schema intentionally keeps this list open for handler-specific instrument extensions; `billing_address` is the standard base field constrained here. |
| `billing_address` | Nested local [`Constraint`](site:schemas/shopping/types/constraint.json) whose `required_fields` values name billing-address fields. |
| `credentials` | Accepted credential families and credential-specific constraints. Entries are typed constraints; concrete instrument schemas can narrow known entries while still allowing handler-specific entries. |
| Axis | Key | Mechanism |
| :--- | :-- | :-------- |
| Field requirements | `constraints` | A [`Constraint`](site:schemas/shopping/types/constraint.json) — a bounded JSON Schema over the instrument's OWN submitted fields: `required` (presence) and `properties.<field>.enum` (allowed values). |
| Accepted options | `accepts` | A uniform map of derived/selected attributes to supported values (e.g. `{ "brand": ["visa","mastercard"] }`). Read to OFFER options; the attribute (e.g. card network) is derived, not a submitted field — a menu, not a field-value constraint. |
| Credential requirements | `credentials` | A typed list keyed by credential `type`, each carrying its own `Constraint`. Applied by data lookup on the submitted credential's `type`; unknown types are handler/extension branches (never a schema `oneOf`). |

Card instruments inherit those base constraints and add card-specific constraints:
Within `constraints`:

| Key | Description |
| :-- | :---------- |
| `brands` | Accepted card network names, such as `visa`, `mastercard`, or `amex`. |
| `credentials` | Refines the base typed credential list with UCP-defined card credential entries while preserving extension credential entries. |
| `required` | Instrument fields that MUST be present (e.g. `billing_address`). |
| `properties` | Per-field nested constraints, recursive — `properties.billing_address.required` names the address fields needed (AVS postal code); `properties.<field>.enum` restricts a field's value (a per-merchant runtime set, not a base-schema enum). |

Use field-level constraints instead of handler-specific booleans when the
requirement is about data that is already modeled by a schema. For example, an
AVS postal-code requirement is expressed as a billing address constraint rather
than a new `requires_billing_postal_code` flag.
Express requirements as field-level constraints instead of handler-specific
booleans. An AVS postal-code requirement is
`constraints.properties.billing_address.required: ["postal_code"]`, not a new
`requires_billing_postal_code` flag. An accepted-brands list is `accepts.brand`,
not a bespoke `brands` key — so accepted-value menus stay uniform instead of
proliferating.

<!-- ucp:example schema=payment_handler def=business_schema -->
```json
Expand All @@ -352,15 +346,20 @@ than a new `requires_billing_postal_code` flag.
{
"type": "card",
"constraints": {
"brands": ["visa", "mastercard"],
"required_fields": ["billing_address"],
"billing_address": {
"required_fields": ["postal_code", "address_country"]
},
"credentials": [
{ "type": "token" }
]
}
"required": ["billing_address"],
"properties": {
"billing_address": {
"required": ["postal_code", "address_country"]
}
}
},
"accepts": {
"brand": ["visa", "mastercard"]
},
"credentials": [
{ "type": "pan", "constraints": { "required": ["cvc"] } },
{ "type": "network_token", "constraints": { "required": ["cryptogram"] } }
]
}
]
}
Expand Down Expand Up @@ -578,15 +577,14 @@ is the payment-instrument application of the generic
that selected branch. Each instrument schema defines its own `available_*`
variant in `$defs` that specializes this typed entry. For example,
[`card_payment_instrument.json`](site:schemas/shopping/types/card_payment_instrument.json)
defines `available_card_payment_instrument` as the `card` branch with
card-specific constraints such as `brands` and card credential refinements. Base
payment-instrument constraints such as `billing_address` and `credentials` also
apply.

| Schema | Constraints |
| :--------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------- |
| [`available_payment_instrument.json`](site:schemas/shopping/types/available_payment_instrument.json) | Base typed entry: type, open `required_fields`, `billing_address`, and `credentials` constraints |
| `card_payment_instrument.json#/$defs/available_card_payment_instrument` | Card branch: `type: "card"`, `brands`, and card credential refinements |
defines `available_card_payment_instrument` as the `card` branch. Card-specific
constraints (`brands`, accepted `credentials`) ride on the open base `Constraint`,
so the card branch only pins `type: "card"`.

| Schema | Constraints |
| :--------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------ |
| [`available_payment_instrument.json`](site:schemas/shopping/types/available_payment_instrument.json) | Base: `type` + a `Constraint` (`required`/`properties`/`enum`) plus a `credentials` typed list |
| `card_payment_instrument.json#/$defs/available_card_payment_instrument` | Card branch: pins `type: "card"`; `brands` and credential entries ride on the open base |

Handlers reference these instrument-defined schemas from
`$defs.{handler_name}.available_payment_instrument` when they need machine
Expand Down
58 changes: 28 additions & 30 deletions source/schemas/shopping/types/available_payment_instrument.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,34 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/shopping/types/available_payment_instrument.json",
"title": "Available Payment Instrument",
"description": "An instrument type available from a payment handler with optional constraints.",
"allOf": [
{ "$ref": "type_constraint.json" },
{
"description": "An instrument type a handler accepts. What it accepts is declared along three distinct axes, each with its own mechanism: `constraints` (a bounded JSON Schema over the instrument's own fields), `accepts` (menus of derived/selected options), and `credentials` (per-credential-type requirements, discriminated by a data lookup).",
"type": "object",
"required": ["type"],
"properties": {
"type": {
"type": "string",
"description": "The instrument type identifier (e.g., 'card')."
},
"constraints": {
"$ref": "constraint.json",
"description": "A Constraint (bounded JSON Schema) over the instrument's OWN submitted fields: `required` (presence) and `properties.<field>.enum` (allowed values). This is the only part that is a runnable schema over the instrument."
},
"accepts": {
"type": "object",
"properties": {
"constraints": {
"allOf": [
{ "$ref": "constraint.json" },
{
"type": "object",
"properties": {
"billing_address": {
"$ref": "constraint.json",
"description": "Local constraint on the instrument's `billing_address` field."
},
"credentials": {
"type": "array",
"items": { "$ref": "type_constraint.json" },
"uniqueItems": true,
"minItems": 1,
"description": "Credential specific constraints accepted for the payment instrument branch selected by `type`. Concrete instrument schemas can narrow known credential entries while preserving handler-extended credential entries."
}
}
}
],
"description": "Constraints on this instrument type. Base payment instrument constraints can require and describe `billing_address` and `credentials`; concrete instrument schemas SHOULD add instrument-specific constraint keys while remaining open to extensions.",
"minProperties": 1
}
}
"additionalProperties": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"minItems": 1
},
"description": "Menus of derived/selected attributes the handler supports (e.g. `{ \"brand\": [\"visa\",\"mastercard\"] }`). Consumers read these to OFFER options; the attribute is derived (e.g. card network from the PAN), not a submitted field, so it is a menu rather than a field-value constraint. A uniform map keyed by attribute, open to new attributes."
},
"credentials": {
"type": "array",
"items": { "$ref": "type_constraint.json" },
"uniqueItems": true,
"minItems": 1,
"description": "Accepted credential families with per-family requirements — each a Typed Constraint keyed by `type`. Discriminated by data lookup on the submitted credential's `type`; unknown types are handler/extension branches. A list, not a schema `oneOf`."
}
]
}
}
28 changes: 2 additions & 26 deletions source/schemas/shopping/types/card_payment_instrument.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,13 @@
"$defs": {
"available_card_payment_instrument": {
"title": "Available Card Payment Instrument",
"description": "Declares card instrument availability with card-specific constraints.",
"description": "Declares card instrument availability. Card-specific constraints ride on the open base Constraint: `constraints.properties.brand.enum` limits accepted card networks, and `constraints.credentials[]` lists accepted credential types (e.g. `pan`, `network_token`) with their required fields. No card-specific schema keys are needed.",
"allOf": [
{ "$ref": "available_payment_instrument.json" },
{
"type": "object",
"properties": {
"type": { "const": "card" },
"constraints": {
"type": "object",
"properties": {
"brands": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"uniqueItems": true,
"description": "Limit to specific card brands (e.g., ['visa', 'mastercard', 'amex'])."
},
"credentials": {
"items": {
"if": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "const": "token" }
}
},
"then": { "$ref": "token_credential.json#/$defs/constraint" }
}
}
}
}
"type": { "const": "card" }
}
}
]
Expand Down
Loading