From 5e0d53ad6ba89e2fcdfea2b39c2794e810aed85f Mon Sep 17 00:00:00 2001 From: Daniel Wyckoff Date: Fri, 21 Aug 2026 01:24:55 -0400 Subject: [PATCH 1/4] introduce Pan vs. CVV state; reformat brand constraints, update constraint examples --- .cspell/custom-words.txt | 1 + docs/documentation/schema-authoring.md | 8 +- docs/specification/overview/index.md | 57 +++++++----- .../encrypted-credential-payment-handler.md | 4 +- .../platform-tokenizer-payment-handler.md | 2 +- docs/specification/payment/guide.md | 86 +++++++++++++++++-- docs/specification/payment/tokenization.md | 8 +- source/handlers/tokenization/openapi.json | 41 ++++----- .../shopping/types/card_credential.json | 5 +- .../types/card_payment_instrument.json | 51 +++++++++-- .../types/network_token_credential.json | 68 +++++++++++++++ .../shopping/types/pan_credential.json | 54 ++++++++++++ 12 files changed, 318 insertions(+), 67 deletions(-) create mode 100644 source/schemas/shopping/types/network_token_credential.json create mode 100644 source/schemas/shopping/types/pan_credential.json diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index 1f228c0f6..7d4ef268d 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -81,6 +81,7 @@ amortiguación atok backorder barcodes +cartebancaire checkout cimd credentialization diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index b94ec28aa..131f7798c 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -544,9 +544,15 @@ registered member. | Object Constraint | `required`, `properties`, `anyOf` | `required` contains unique field names; `properties` maps field names to Object or Value Constraints; `anyOf` is a non-empty array of non-empty Object Constraints; an empty object is a no-op except as an `anyOf` branch. | | Value Constraint | `enum`, `const` | `enum` is non-empty and unique; at least one member is present; both apply when present together. | -The listed members define the grammar. A `request_constraints` value may +The listed members define the grammar. A `request_constraints` element may additionally contain `path` at the top level. +The registered member is an **array** of these elements, because one scope often +has more than one rule and each rule needs its own `path`. Elements compose +conjunctively, order carries no meaning, and each is validated independently. A +Business **MUST** ensure elements that can select the same object are jointly +satisfiable. + Grammar changes belong in `constraint_expression.json`, and changes to the root Object Constraint must also be reflected in `request_constraints.json`. diff --git a/docs/specification/overview/index.md b/docs/specification/overview/index.md index dc2498578..92e8bb53e 100644 --- a/docs/specification/overview/index.md +++ b/docs/specification/overview/index.md @@ -184,6 +184,11 @@ Platform request. For example, it can constrain a Line Item quantity to exactly avoiding a round trip for request data the Business has already indicated it will reject. +`ucp.request_constraints` is an array. Each element carries one optional `path` +and one Constraint Expression, so a single scope can state several rules with +different targets. Elements compose conjunctively, array order carries no +meaning, and a consumer **MUST NOT** read precedence into position. + `ucp.request_constraints` is used only in authoritative operation responses and has no effect in discovery profiles or operation requests. @@ -367,12 +372,14 @@ The following Cart responses are alternatives that illustrate different scopes. { "ucp": { "version": "{{ ucp_version }}", - "request_constraints": { - "path": "$['line_items'][*]", - "properties": { - "quantity": {"const": 1} + "request_constraints": [ + { + "path": "$['line_items'][*]", + "properties": { + "quantity": {"const": 1} + } } - } + ] }, "id": "cart_123", "line_items": [ @@ -421,12 +428,14 @@ The following Cart responses are alternatives that illustrate different scopes. {"type": "total", "amount": 120000} ], "ucp": { - "request_constraints": { - "path": "$['line_items'][?@['id'] == 'line_123']", - "properties": { - "quantity": {"const": 100} + "request_constraints": [ + { + "path": "$['line_items'][?@['id'] == 'line_123']", + "properties": { + "quantity": {"const": 100} + } } - } + ] } } ], @@ -469,17 +478,19 @@ derived path is `$`, which selects the next request root. ] }, "payment_handlers": {}, - "request_constraints": { - "required": ["discounts"], - "properties": { - "discounts": { - "required": ["codes"], - "properties": { - "codes": {"const": ["ACME-X7Q9-L2M4"]} + "request_constraints": [ + { + "required": ["discounts"], + "properties": { + "discounts": { + "required": ["codes"], + "properties": { + "codes": {"const": ["ACME-X7Q9-L2M4"]} + } } } } - } + ] }, "id": "checkout_123", "status": "incomplete", @@ -530,10 +541,12 @@ matching submitted card instrument: { "type": "card", "ucp": { - "request_constraints": { - "path": "$['payment']['instruments'][?@['handler_id'] == 'processor_1' && @['type'] == 'card']", - "required": ["billing_address"] - } + "request_constraints": [ + { + "path": "$['payment']['instruments'][?@['handler_id'] == 'processor_1' && @['type'] == 'card']", + "required": ["billing_address"] + } + ] } } ``` diff --git a/docs/specification/payment/examples/encrypted-credential-payment-handler.md b/docs/specification/payment/examples/encrypted-credential-payment-handler.md index 58779ce83..70a42b24b 100644 --- a/docs/specification/payment/examples/encrypted-credential-payment-handler.md +++ b/docs/specification/payment/examples/encrypted-credential-payment-handler.md @@ -121,11 +121,11 @@ Businesses advertise the platform's handler. The `business_id` field identifies the business, which the platform uses to look up the correct public key for encryption. -The only supported instrument schema is [CardPaymentInstrument](site:schemas/shopping/types/card_payment_instrument.json), the only supported checkout credential schema is `EncryptedCredential`, and the only supported source credential schema is [CardCredential](site:schemas/shopping/types/card_credential.json). +The only supported instrument schema is [CardPaymentInstrument](site:schemas/shopping/types/card_payment_instrument.json), the only supported checkout credential schema is `EncryptedCredential`, and the only supported source credential schema is [PanCredential](site:schemas/shopping/types/pan_credential.json). **Note:** The `EncryptedCredential` shape would be formally defined in the handler's schema (referenced via the `schema` field in the handler declaration). -**Note:** `CardCredential` contains raw PANs. For card credentials, the +**Note:** `PanCredential` contains raw PANs. For card credentials, the platform's vaulting service must be **PCI DSS compliant** when handling these credentials. Businesses receive only encrypted payloads but must be PCI DSS compliant once they decrypt card credentials locally. Other credential types diff --git a/docs/specification/payment/examples/platform-tokenizer-payment-handler.md b/docs/specification/payment/examples/platform-tokenizer-payment-handler.md index 67a1aa751..325472a43 100644 --- a/docs/specification/payment/examples/platform-tokenizer-payment-handler.md +++ b/docs/specification/payment/examples/platform-tokenizer-payment-handler.md @@ -176,7 +176,7 @@ platform's handler specification (referenced via `spec`) documents the `/detokenize` endpoint URL exposed by the platform's **payment credential provider**. -The handler accepts [CardCredential](site:schemas/shopping/types/card_credential.json) for tokenization and produces [TokenCredential](site:schemas/shopping/types/token_credential.json) for checkout. +The handler accepts [PanCredential](site:schemas/shopping/types/pan_credential.json) and [NetworkTokenCredential](site:schemas/shopping/types/network_token_credential.json) for tokenization and produces [TokenCredential](site:schemas/shopping/types/token_credential.json) for checkout. **Note:** The result of `/detokenize` contains **sensitive payment data**. Both the sender (platform's credential provider) and receiver diff --git a/docs/specification/payment/guide.md b/docs/specification/payment/guide.md index 0aaaecbb0..7db2b162e 100644 --- a/docs/specification/payment/guide.md +++ b/docs/specification/payment/guide.md @@ -194,6 +194,25 @@ brands to `["visa", "mastercard"]`). In a Business profile and authoritative response, array order communicates preferred instrument presentation, earliest first. +Each entry narrows along two axes: + +```text +available_instruments[] +├── type selects an instrument schema +├── constraints derived values, not submitted (card: brand) +└── ucp.request_constraints[] requirements on submitted instruments and credentials +``` + +`constraints` carries values the Business derives rather than receives, so they +cannot be expressed as requirements on request data. The card availability +schema defines `brand`, the set of accepted card schemes, which is derived from +the account number and survives tokenization. + +`ucp.request_constraints` carries everything that *is* on the wire: which +instrument fields are required, which credential types are accepted, and which +fields each of those credential types must carry. Use field requirements rather +than handler-specific booleans for modeled data. + When an authoritative response includes `ucp.request_constraints` on an available instrument, the Business **MUST** include an explicit `path` because the available instrument's response Normalized Path does not identify submitted @@ -203,6 +222,49 @@ to submitted `handler_id` and the available instrument's `type` to submitted Payment-handler and instrument specifications define any stronger association the query needs. See [Request Constraints](../overview/index.md#request-constraints). +Because the member is an array, one available instrument can carry several +rules with different paths. A rule that applies to every instrument of the type +filters on `type` alone; a rule that applies only to one credential family adds +a comparison on `credential.type`. Splitting the credential families into +distinct schemas is what makes that comparison possible, so no rule has to +branch on a sibling field. + + +```json +{ + "id": "processor_tokenizer_1234", + "version": "{{ ucp_version }}", + "available_instruments": [ + { + "type": "card", + "constraints": { "brand": { "enum": ["visa", "mastercard"] } }, + "ucp": { + "request_constraints": [ + { + "path": "$['payment']['instruments'][?@['handler_id'] == 'processor_tokenizer_1234' && @['type'] == 'card']", + "required": ["billing_address", "credential"], + "properties": { + "billing_address": { "required": ["postal_code", "address_country"] }, + "credential": { "properties": { "type": { "enum": ["pan", "network_token"] } } } + } + }, + { + "path": "$['payment']['instruments'][?@['handler_id'] == 'processor_tokenizer_1234' && @['credential']['type'] == 'pan']", + "properties": { "credential": { "required": ["cvc"] } } + } + ] + } + } + ] +} +``` + +The first rule applies to every card instrument from this handler; the second +requires a CVC only when the submitted credential is a PAN, and leaves a network +token untouched. Declared constraints are the upfront minimum; dynamic +requirements still use recoverable errors and +[`message_error.path`](site:schemas/common/types/message_error.json). + --- #### Handler Declaration Variants @@ -522,16 +584,23 @@ multiple instrument types for different payment flows. Each instrument schema defines its own `available_*` variant in `$defs` that specifies what constraints are valid for that instrument type. For example, [`card_payment_instrument.json`](site:schemas/shopping/types/card_payment_instrument.json) -defines `available_card_payment_instrument` with a `brands` constraint. +defines `available_card_payment_instrument` with a `brand` constraint. | Schema | Constraints | | :----------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | | [`available_payment_instrument.json`](site:schemas/shopping/types/available_payment_instrument.json) | Base: type, constraints (open object) | -| `card_payment_instrument.json#/$defs/available_card_payment_instrument` | Extends base with `constraints.brands` for card networks | +| `card_payment_instrument.json#/$defs/available_card_payment_instrument` | Extends base with `constraints.brand` for card schemes | Handlers reference these instrument-defined schemas when declaring `available_instruments`. The **instrument schema authors** define what -constraints are meaningful (e.g., `brands` for cards), and **platforms/businesses** use this to advertise what they support (e.g., `["visa", "mastercard"]`). +constraints are meaningful (e.g., `brand` for cards), and **platforms/businesses** use this to advertise what they support (e.g., `{ "enum": ["visa", "mastercard"] }`). + +The base Payment Handler intentionally remains open: an instrument `type` does +not globally select a schema, so a card-specific constraint is only validated +where a handler schema names the card definition. A handler that expects its +`available_instruments` entries to be validated **SHOULD** reference the +instrument's `available_*` definition and declare the optional `ucp` property, +which is what brings `request_constraints` into the validated path. **Example `types/tokenizer_instrument.json`**: @@ -553,6 +622,7 @@ constraints are meaningful (e.g., `brands` for cards), and **platforms/businesse "type": "object", "properties": { "type": { "const": "tokenizer_card" }, + "ucp": { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/ucp.json#/$defs/members" }, "constraints": { "type": "object", "properties": { @@ -626,10 +696,12 @@ constraints are meaningful (e.g., `brands` for cards), and **platforms/businesse **Base Credential Schemas:** -| Schema | Description | -| :------------------------------------------------------------------------------- | :---------------------------- | -| [`payment_credential.json`](site:schemas/shopping/types/payment_credential.json) | Base: type discriminator only | -| [`token_credential.json`](site:schemas/shopping/types/token_credential.json) | Token: type + token string | +| Schema | Description | +| :------------------------------------------------------------------------------------------- | :------------------------------------------ | +| [`payment_credential.json`](site:schemas/shopping/types/payment_credential.json) | Base: type discriminator only | +| [`token_credential.json`](site:schemas/shopping/types/token_credential.json) | Token: type + token string | +| [`pan_credential.json`](site:schemas/shopping/types/pan_credential.json) | Raw FPAN, verified with `cvc`. Source only. | +| [`network_token_credential.json`](site:schemas/shopping/types/network_token_credential.json) | Network token, verified with `cryptogram` | UCP provides base schemas for universal payment credentials. Authors **MAY** extend these schemas to include handler-specific credential context. Handlers diff --git a/docs/specification/payment/tokenization.md b/docs/specification/payment/tokenization.md index a99f3ee40..d9b2a0a62 100644 --- a/docs/specification/payment/tokenization.md +++ b/docs/specification/payment/tokenization.md @@ -123,8 +123,7 @@ Content-Type: application/json { "credential": { - "type": "card", - "card_number_type": "fpan", + "type": "pan", "number": "4111111111111111", "expiry_month": 12, "expiry_year": 2026, @@ -174,8 +173,7 @@ Authorization: Bearer {caller_access_token} ```json { - "type": "card", - "card_number_type": "fpan", + "type": "pan", "number": "4111111111111111", "expiry_month": 12, "expiry_year": 2026, @@ -235,7 +233,7 @@ When publishing your handler, your specification document **MUST** include: | Instrument | Source Credentials | Checkout Credentials | | :--------- | :--------------------------- | :------------------- | -| `card` | `card` (fpan, network_token) | `token` | +| `card` | `pan`, `network_token` | `token` | **Token Lifecycle:** Single-use (invalidated after detokenization) diff --git a/source/handlers/tokenization/openapi.json b/source/handlers/tokenization/openapi.json index 3a5f29859..e60d6a1ea 100644 --- a/source/handlers/tokenization/openapi.json +++ b/source/handlers/tokenization/openapi.json @@ -30,12 +30,11 @@ } }, "examples": { - "card_credential_agent": { - "summary": "Agent tokenizing on behalf of merchant", + "pan_credential_agent": { + "summary": "Agent tokenizing a PAN credential on behalf of merchant", "value": { "credential": { - "type": "card", - "card_number_type": "fpan", + "type": "pan", "number": "4111111111111111", "expiry_month": 12, "expiry_year": 2026, @@ -50,12 +49,11 @@ } } }, - "card_credential_merchant": { - "summary": "Merchant tokenizing for themselves", + "pan_credential_merchant": { + "summary": "Merchant tokenizing a PAN credential for themselves", "value": { "credential": { - "type": "card", - "card_number_type": "fpan", + "type": "pan", "number": "4111111111111111", "expiry_month": 12, "expiry_year": 2026, @@ -67,17 +65,17 @@ } } }, - "card_credential_network_token": { - "summary": "Card credential (network token)", + "network_token_credential_agent": { + "summary": "Agent tokenizing a network token credential", "value": { "credential": { - "type": "card", - "card_number_type": "network_token", - "number": "4111111111111111", + "type": "network_token", + "number": "5204240000004242", "expiry_month": 12, "expiry_year": 2026, "cryptogram": "gXc5UCLnM6ckD7pjM1TdPA==", "eci_value": "07", + "token_requestor_id": "12345678901", "name": "Jane Doe" }, "binding": { @@ -179,11 +177,10 @@ "$ref": "../../schemas/shopping/types/payment_credential.json" }, "examples": { - "card_credential_fpan": { - "summary": "Card credential (FPAN)", + "pan_credential": { + "summary": "PAN credential", "value": { - "type": "card", - "card_number_type": "fpan", + "type": "pan", "number": "4111111111111111", "expiry_month": 12, "expiry_year": 2026, @@ -191,16 +188,16 @@ "name": "Jane Doe" } }, - "card_credential_network_token": { - "summary": "Card credential (network token)", + "network_token_credential": { + "summary": "Network token credential", "value": { - "type": "card", - "card_number_type": "network_token", - "number": "4111111111111111", + "type": "network_token", + "number": "5204240000004242", "expiry_month": 12, "expiry_year": 2026, "cryptogram": "gXc5UCLnM6ckD7pjM1TdPA==", "eci_value": "07", + "token_requestor_id": "12345678901", "name": "Jane Doe" } } diff --git a/source/schemas/shopping/types/card_credential.json b/source/schemas/shopping/types/card_credential.json index 0d08cc790..4c87b1077 100644 --- a/source/schemas/shopping/types/card_credential.json +++ b/source/schemas/shopping/types/card_credential.json @@ -2,7 +2,8 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/shopping/types/card_credential.json", "title": "Card Credential", - "description": "A card credential containing sensitive payment card details including raw Primary Account Numbers (PANs). This credential type MUST NOT be used for checkout, only with payment handlers that tokenize or encrypt credentials. CRITICAL: Both parties handling CardCredential (sender and receiver) MUST be PCI DSS compliant. Transmission MUST use HTTPS/TLS with strong cipher suites.", + "description": "Deprecated: use PAN Credential (`pan_credential.json`) or Network Token Credential (`network_token_credential.json`). A card credential containing sensitive payment card details including raw Primary Account Numbers (PANs). This credential type MUST NOT be used for checkout, only with payment handlers that tokenize or encrypt credentials. CRITICAL: Both parties handling CardCredential (sender and receiver) MUST be PCI DSS compliant. Transmission MUST use HTTPS/TLS with strong cipher suites.", + "deprecated": true, "allOf": [ { "$ref": "payment_credential.json" @@ -25,7 +26,7 @@ "network_token", "dpan" ], - "description": "The type of card number. Network tokens are preferred with fallback to FPAN. See PCI Scope for more details." + "description": "Deprecated: the credential type now carries this distinction. The type of card number. Network tokens are preferred with fallback to FPAN. See PCI Scope for more details." }, "number": { "type": "string", diff --git a/source/schemas/shopping/types/card_payment_instrument.json b/source/schemas/shopping/types/card_payment_instrument.json index 2704308bc..bf130c14d 100644 --- a/source/schemas/shopping/types/card_payment_instrument.json +++ b/source/schemas/shopping/types/card_payment_instrument.json @@ -8,7 +8,9 @@ "title": "Available Card Payment Instrument", "description": "Declares card instrument availability with card-specific constraints.", "allOf": [ - { "$ref": "available_payment_instrument.json" }, + { + "$ref": "available_payment_instrument.json" + }, { "type": "object", "properties": { @@ -20,10 +22,43 @@ "properties": { "brands": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "minItems": 1, "uniqueItems": true, - "description": "Limit to specific card brands (e.g., ['visa', 'mastercard', 'amex'])." + "description": "Limit to specific card brands (e.g., ['visa', 'mastercard', 'amex']).", + "deprecated": true + }, + "brand": { + "title": "Card Brand Constraint", + "description": "Card schemes accepted. Derived from the account number, not submitted.", + "allOf": [ + { + "$ref": "../../common/types/constraint_expression.json#/$defs/value_constraint" + } + ], + "properties": { + "enum": { + "items": { + "type": "string" + } + }, + "const": { + "type": "string" + } + }, + "examples": [ + { + "enum": [ + "visa", + "mastercard" + ] + }, + { + "const": "cartebancaire" + } + ] } } } @@ -38,20 +73,26 @@ }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", "const": "card", "description": "Indicates this is a card payment instrument." }, + "network": { + "type": "string", + "description": "Card network elected for this transaction, typically a co-badged selection. When present, the business MAY decline if the card cannot route over it and MUST NOT substitute another." + }, "display": { "type": "object", "description": "Display information for this card payment instrument.", "properties": { "brand": { "type": "string", - "description": "The card brand/network (e.g., visa, mastercard, amex)." + "description": "The card brand/network shown to the buyer (e.g., visa, mastercard, amex). Presentational only." }, "last_digits": { "type": "string", diff --git a/source/schemas/shopping/types/network_token_credential.json b/source/schemas/shopping/types/network_token_credential.json new file mode 100644 index 000000000..02075a3ad --- /dev/null +++ b/source/schemas/shopping/types/network_token_credential.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/network_token_credential.json", + "title": "Network Token Credential", + "description": "A card-network token credential verified with a transaction cryptogram. The `number` field carries the network token or wallet-provisioned token rather than the underlying FPAN.", + "allOf": [ + { + "$ref": "payment_credential.json" + }, + { + "type": "object", + "required": [ + "type", + "number", + "cryptogram" + ], + "properties": { + "type": { + "const": "network_token", + "description": "The credential type identifier for network token credentials." + }, + "number": { + "type": "string", + "description": "Network token or wallet-provisioned token replacing the underlying FPAN.", + "examples": [ + "5204240000004242" + ] + }, + "expiry_month": { + "type": "integer", + "description": "The month of the token's expiration date (1-12)." + }, + "expiry_year": { + "type": "integer", + "description": "The year of the token's expiration date." + }, + "name": { + "type": "string", + "description": "Cardholder name.", + "examples": [ + "Jane Doe" + ] + }, + "cryptogram": { + "type": "string", + "description": "Transaction cryptogram or dynamic CVC (dCVV), in the long or short form expected by the card network or processor.", + "examples": [ + "gXc5UCLnM6ckD7pjM1TdPA==" + ] + }, + "eci_value": { + "type": "string", + "description": "Electronic Commerce Indicator / Security Level Indicator associated with the transaction.", + "examples": [ + "07" + ] + }, + "token_requestor_id": { + "type": "string", + "description": "Payment network token requestor identifier, when required by the processor or network-token program.", + "examples": [ + "12345678901" + ] + } + } + } + ] +} diff --git a/source/schemas/shopping/types/pan_credential.json b/source/schemas/shopping/types/pan_credential.json new file mode 100644 index 000000000..52de4b9f6 --- /dev/null +++ b/source/schemas/shopping/types/pan_credential.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/pan_credential.json", + "title": "PAN Credential", + "description": "A card credential carrying a funding primary account number (FPAN). This credential type MUST NOT be used for checkout, only with payment handlers that tokenize or encrypt credentials. CRITICAL: Both parties handling a PAN credential (sender and receiver) MUST be PCI DSS compliant. Transmission MUST use HTTPS/TLS with strong cipher suites.", + "allOf": [ + { + "$ref": "payment_credential.json" + }, + { + "type": "object", + "required": [ + "type", + "number" + ], + "properties": { + "type": { + "const": "pan", + "description": "The credential type identifier for PAN credentials." + }, + "number": { + "type": "string", + "description": "Funding primary account number (FPAN).", + "examples": [ + "4242424242424242" + ] + }, + "expiry_month": { + "type": "integer", + "description": "The month of the card's expiration date (1-12)." + }, + "expiry_year": { + "type": "integer", + "description": "The year of the card's expiration date." + }, + "name": { + "type": "string", + "description": "Cardholder name.", + "examples": [ + "Jane Doe" + ] + }, + "cvc": { + "type": "string", + "maxLength": 4, + "description": "Card verification code.", + "examples": [ + "223" + ] + } + } + } + ] +} From c937b67168f158d3da68d6c26906468c3465fb93 Mon Sep 17 00:00:00 2001 From: Daniel Wyckoff Date: Fri, 21 Aug 2026 08:07:32 -0400 Subject: [PATCH 2/4] doc improvements --- docs/documentation/schema-authoring.md | 8 +-- docs/specification/overview/index.md | 61 ++++++++----------- docs/specification/payment/guide.md | 45 +++++++------- docs/specification/payment/tokenization.md | 12 +++- .../shopping/types/pan_credential.json | 2 +- 5 files changed, 59 insertions(+), 69 deletions(-) diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index 131f7798c..b94ec28aa 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -544,15 +544,9 @@ registered member. | Object Constraint | `required`, `properties`, `anyOf` | `required` contains unique field names; `properties` maps field names to Object or Value Constraints; `anyOf` is a non-empty array of non-empty Object Constraints; an empty object is a no-op except as an `anyOf` branch. | | Value Constraint | `enum`, `const` | `enum` is non-empty and unique; at least one member is present; both apply when present together. | -The listed members define the grammar. A `request_constraints` element may +The listed members define the grammar. A `request_constraints` value may additionally contain `path` at the top level. -The registered member is an **array** of these elements, because one scope often -has more than one rule and each rule needs its own `path`. Elements compose -conjunctively, order carries no meaning, and each is validated independently. A -Business **MUST** ensure elements that can select the same object are jointly -satisfiable. - Grammar changes belong in `constraint_expression.json`, and changes to the root Object Constraint must also be reflected in `request_constraints.json`. diff --git a/docs/specification/overview/index.md b/docs/specification/overview/index.md index 92e8bb53e..33dcd6ac4 100644 --- a/docs/specification/overview/index.md +++ b/docs/specification/overview/index.md @@ -184,11 +184,6 @@ Platform request. For example, it can constrain a Line Item quantity to exactly avoiding a round trip for request data the Business has already indicated it will reject. -`ucp.request_constraints` is an array. Each element carries one optional `path` -and one Constraint Expression, so a single scope can state several rules with -different targets. Elements compose conjunctively, array order carries no -meaning, and a consumer **MUST NOT** read precedence into position. - `ucp.request_constraints` is used only in authoritative operation responses and has no effect in discovery profiles or operation requests. @@ -372,14 +367,12 @@ The following Cart responses are alternatives that illustrate different scopes. { "ucp": { "version": "{{ ucp_version }}", - "request_constraints": [ - { - "path": "$['line_items'][*]", - "properties": { - "quantity": {"const": 1} - } + "request_constraints": { + "path": "$['line_items'][*]", + "properties": { + "quantity": {"const": 1} } - ] + } }, "id": "cart_123", "line_items": [ @@ -428,14 +421,12 @@ The following Cart responses are alternatives that illustrate different scopes. {"type": "total", "amount": 120000} ], "ucp": { - "request_constraints": [ - { - "path": "$['line_items'][?@['id'] == 'line_123']", - "properties": { - "quantity": {"const": 100} - } + "request_constraints": { + "path": "$['line_items'][?@['id'] == 'line_123']", + "properties": { + "quantity": {"const": 100} } - ] + } } } ], @@ -478,19 +469,17 @@ derived path is `$`, which selects the next request root. ] }, "payment_handlers": {}, - "request_constraints": [ - { - "required": ["discounts"], - "properties": { - "discounts": { - "required": ["codes"], - "properties": { - "codes": {"const": ["ACME-X7Q9-L2M4"]} - } + "request_constraints": { + "required": ["discounts"], + "properties": { + "discounts": { + "required": ["codes"], + "properties": { + "codes": {"const": ["ACME-X7Q9-L2M4"]} } } } - ] + } }, "id": "checkout_123", "status": "incomplete", @@ -541,12 +530,10 @@ matching submitted card instrument: { "type": "card", "ucp": { - "request_constraints": [ - { - "path": "$['payment']['instruments'][?@['handler_id'] == 'processor_1' && @['type'] == 'card']", - "required": ["billing_address"] - } - ] + "request_constraints": { + "path": "$['payment']['instruments'][?@['handler_id'] == 'processor_1' && @['type'] == 'card']", + "required": ["billing_address"] + } } } ``` @@ -2867,6 +2854,10 @@ Most platform implementations can **avoid PCI-DSS scope** by: - Forwarding credentials without the ability to use them directly - Using PSP tokenization payment handlers where raw credentials never pass through the platform +- Presenting pre-provisioned card network tokens + (`network_token_credential.json`) rather than an FPAN — the platform never + shares the underlying account number, and the token is unusable without a + matching cryptogram #### Business Scope diff --git a/docs/specification/payment/guide.md b/docs/specification/payment/guide.md index 7db2b162e..e4e9102b0 100644 --- a/docs/specification/payment/guide.md +++ b/docs/specification/payment/guide.md @@ -200,7 +200,7 @@ Each entry narrows along two axes: available_instruments[] ├── type selects an instrument schema ├── constraints derived values, not submitted (card: brand) -└── ucp.request_constraints[] requirements on submitted instruments and credentials +└── ucp.request_constraints requirements on submitted instruments and credentials ``` `constraints` carries values the Business derives rather than receives, so they @@ -222,12 +222,11 @@ to submitted `handler_id` and the available instrument's `type` to submitted Payment-handler and instrument specifications define any stronger association the query needs. See [Request Constraints](../overview/index.md#request-constraints). -Because the member is an array, one available instrument can carry several -rules with different paths. A rule that applies to every instrument of the type -filters on `type` alone; a rule that applies only to one credential family adds -a comparison on `credential.type`. Splitting the credential families into -distinct schemas is what makes that comparison possible, so no rule has to -branch on a sibling field. +One path selects the submitted instrument, and one Constraint Expression +describes it. Requirements shared by every instrument of the type are sibling +members; requirements that differ per credential family are `anyOf` branches on +the nested `credential` object. Splitting the credential families into distinct +schemas is what gives each branch a stable discriminator. ```json @@ -239,30 +238,30 @@ branch on a sibling field. "type": "card", "constraints": { "brand": { "enum": ["visa", "mastercard"] } }, "ucp": { - "request_constraints": [ - { - "path": "$['payment']['instruments'][?@['handler_id'] == 'processor_tokenizer_1234' && @['type'] == 'card']", - "required": ["billing_address", "credential"], - "properties": { - "billing_address": { "required": ["postal_code", "address_country"] }, - "credential": { "properties": { "type": { "enum": ["pan", "network_token"] } } } + "request_constraints": { + "path": "$['payment']['instruments'][?@['handler_id'] == 'processor_tokenizer_1234' && @['type'] == 'card']", + "required": ["billing_address", "credential"], + "properties": { + "billing_address": { "required": ["postal_code", "address_country"] }, + "credential": { + "anyOf": [ + { "properties": { "type": { "const": "pan" } }, "required": ["cvc"] }, + { "properties": { "type": { "const": "network_token" } } } + ] } - }, - { - "path": "$['payment']['instruments'][?@['handler_id'] == 'processor_tokenizer_1234' && @['credential']['type'] == 'pan']", - "properties": { "credential": { "required": ["cvc"] } } } - ] + } } } ] } ``` -The first rule applies to every card instrument from this handler; the second -requires a CVC only when the submitted credential is a PAN, and leaves a network -token untouched. Declared constraints are the upfront minimum; dynamic -requirements still use recoverable errors and +The sibling members apply to every card instrument from this handler. The +branches accept either a PAN credential carrying a `cvc` or a network token, +which `network_token_credential.json` already requires to carry a `cryptogram`; +a credential of any other type satisfies neither branch. Declared constraints +are the upfront minimum; dynamic requirements still use recoverable errors and [`message_error.path`](site:schemas/common/types/message_error.json). --- diff --git a/docs/specification/payment/tokenization.md b/docs/specification/payment/tokenization.md index d9b2a0a62..baea16c65 100644 --- a/docs/specification/payment/tokenization.md +++ b/docs/specification/payment/tokenization.md @@ -57,17 +57,23 @@ Tokenization handlers transform credentials between source and checkout forms: | | source_ | | checkout_ | | | | credentials | What goes IN | credentials | | | | |<--------------- | | | -| | * card/fpan | | What comes OUT | | -| | * card/dpan | ----->| * token | | +| | * pan | | What comes OUT | | +| | * network_token | ----->| * token | | | | | | | | | +-----------------+ +-------------------------+ | | | +-------------------------------------------------------------------------+ ``` -Tokenization handlers accept source credentials (e.g., card with FPAN) and +Tokenization handlers accept source credentials (e.g., a PAN credential) and produce checkout credentials (e.g., tokens). +A network token does not always need this round-trip. A handler **MAY** accept +`network_token_credential.json` as a source credential for tokenization, or +**MAY** accept it directly as a checkout credential, since the token is +unusable without a matching cryptogram. The handler's specification **MUST** +state which of the two it accepts. + ### Token Lifecycle Tokens move through distinct phases. Your handler specification must document diff --git a/source/schemas/shopping/types/pan_credential.json b/source/schemas/shopping/types/pan_credential.json index 52de4b9f6..b392ee522 100644 --- a/source/schemas/shopping/types/pan_credential.json +++ b/source/schemas/shopping/types/pan_credential.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/shopping/types/pan_credential.json", "title": "PAN Credential", - "description": "A card credential carrying a funding primary account number (FPAN). This credential type MUST NOT be used for checkout, only with payment handlers that tokenize or encrypt credentials. CRITICAL: Both parties handling a PAN credential (sender and receiver) MUST be PCI DSS compliant. Transmission MUST use HTTPS/TLS with strong cipher suites.", + "description": "A card credential carrying a funding primary account number (FPAN). Credential selection follows the shape of the value on the wire rather than its provenance: a network token surfaced in PAN form and verified with a `cvc` \u2014 as with credentials where a dynamic verification code proxies the cryptogram \u2014 is carried here, while a token verified with a discrete `cryptogram` uses Network Token Credential. This credential type MUST NOT be used for checkout, only with payment handlers that tokenize or encrypt credentials. CRITICAL: Both parties handling a PAN credential (sender and receiver) MUST be PCI DSS compliant. Transmission MUST use HTTPS/TLS with strong cipher suites.", "allOf": [ { "$ref": "payment_credential.json" From ded6c869eeccb8fe50d30bbd2fd7d6d2f399c416 Mon Sep 17 00:00:00 2001 From: Daniel Wyckoff Date: Fri, 21 Aug 2026 18:07:37 -0400 Subject: [PATCH 3/4] refactor grammar to always be constraints; land on making this change breaking --- docs/documentation/schema-authoring.md | 2 +- docs/specification/overview/index.md | 14 ++- .../encrypted-credential-payment-handler.md | 6 +- .../platform-tokenizer-payment-handler.md | 6 +- .../processor-tokenizer-payment-handler.md | 6 +- docs/specification/payment/guide.md | 88 ++++++++----------- docs/specification/shopping/checkout/mcp.md | 2 +- .../types/available_payment_instrument.json | 6 +- .../types/card_payment_instrument.json | 74 +++------------- 9 files changed, 76 insertions(+), 128 deletions(-) diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index b94ec28aa..eabaeb20c 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -126,7 +126,7 @@ Define payment handler configurations in `ucp.payment_handlers{}` registries. - **Top-level fields**: `$schema`, `$id`, `title`, `description`, `name`, `version`, `available_instruments` - **Variants**: `platform_schema`, `business_schema`, `response_schema` - **Instance `id`**: Required to distinguish multiple configurations of the same handler -- **`available_instruments`**: Optional. Array of supported instrument types with type-specific constraints (e.g., brands for credit cards). When absent, the handler places no restrictions — it supports the full set of instrument types defined by its handler schema. +- **`available_instruments`**: Optional. Array of supported instrument types, each with an optional Constraint Expression over type-specific members (e.g., `brand` for credit cards). When absent, the handler places no restrictions — it supports the full set of instrument types defined by its handler schema. Examples: `com.google.pay`, `dev.shopify.shop_pay`, `dev.ucp.processor_tokenizer` diff --git a/docs/specification/overview/index.md b/docs/specification/overview/index.md index 33dcd6ac4..d7892375f 100644 --- a/docs/specification/overview/index.md +++ b/docs/specification/overview/index.md @@ -244,6 +244,14 @@ that member is present, so a branch pinning a discriminator through discriminator in the branch's `required` makes the branch match only the shape it describes. +The grammar is defined independently of the object it is bound to. Request +Constraints bind it to objects in the next request and add `path`; other UCP +schemas reuse it where a declaration already identifies the object it +constrains, such as +[`available_instruments[].constraints`](site:schemas/shopping/types/available_payment_instrument.json), +whose object is the `constraint_target` declared by the instrument schema for +that entry's `type`. + ### Path Every `request_constraints` value has exactly one effective path. When `path` @@ -1356,7 +1364,7 @@ Businesses publish their profile at `/.well-known/ucp`. An example: { "type": "card", "constraints": { - "brands": ["visa", "mastercard", "amex"] + "properties": { "brand": { "enum": ["visa", "mastercard", "amex"] } } } } ], @@ -1517,7 +1525,7 @@ example: "spec": "https://example.com/specs/payments/processor_tokenizer-payment", "schema": "https://example.com/schemas/payments/delegate-payment.json", "available_instruments": [ - {"type": "card", "constraints": {"brands": ["visa", "mastercard"]}} + {"type": "card", "constraints": {"properties": {"brand": {"enum": ["visa", "mastercard"]}}}} ] } ] @@ -2709,7 +2717,7 @@ request a challenge. { "type": "card", "constraints": { - "brands": ["visa", "mastercard"] + "properties": { "brand": { "enum": ["visa", "mastercard"] } } } } ], diff --git a/docs/specification/payment/examples/encrypted-credential-payment-handler.md b/docs/specification/payment/examples/encrypted-credential-payment-handler.md index 70a42b24b..1aaffb150 100644 --- a/docs/specification/payment/examples/encrypted-credential-payment-handler.md +++ b/docs/specification/payment/examples/encrypted-credential-payment-handler.md @@ -157,7 +157,7 @@ have their own compliance requirements. { "type": "card", "constraints": { - "brands": ["visa", "mastercard"] + "properties": { "brand": { "enum": ["visa", "mastercard"] } } } } ], @@ -195,7 +195,7 @@ The response config includes information about the encryption used. { "type": "card", "constraints": { - "brands": ["visa", "mastercard"] + "properties": { "brand": { "enum": ["visa", "mastercard"] } } } } ], @@ -269,7 +269,7 @@ registry using `platform_config`. { "type": "card", "constraints": { - "brands": ["visa", "mastercard", "amex", "discover"] + "properties": { "brand": { "enum": ["visa", "mastercard", "amex", "discover"] } } } } ], diff --git a/docs/specification/payment/examples/platform-tokenizer-payment-handler.md b/docs/specification/payment/examples/platform-tokenizer-payment-handler.md index 325472a43..3043c0e4b 100644 --- a/docs/specification/payment/examples/platform-tokenizer-payment-handler.md +++ b/docs/specification/payment/examples/platform-tokenizer-payment-handler.md @@ -208,7 +208,7 @@ credential type (e.g., PCI DSS for cards). { "type": "card", "constraints": { - "brands": ["visa", "mastercard"] + "properties": { "brand": { "enum": ["visa", "mastercard"] } } } } ], @@ -244,7 +244,7 @@ The response config includes runtime token lifecycle information. { "type": "card", "constraints": { - "brands": ["visa", "mastercard"] + "properties": { "brand": { "enum": ["visa", "mastercard"] } } } } ], @@ -342,7 +342,7 @@ registry using `platform_config`. { "type": "card", "constraints": { - "brands": ["visa", "mastercard", "amex", "discover"] + "properties": { "brand": { "enum": ["visa", "mastercard", "amex", "discover"] } } } } ], diff --git a/docs/specification/payment/examples/processor-tokenizer-payment-handler.md b/docs/specification/payment/examples/processor-tokenizer-payment-handler.md index deac8e020..bc5252186 100644 --- a/docs/specification/payment/examples/processor-tokenizer-payment-handler.md +++ b/docs/specification/payment/examples/processor-tokenizer-payment-handler.md @@ -123,7 +123,7 @@ The handler's specification (referenced via the `spec` field) documents the { "type": "card", "constraints": { - "brands": ["visa", "mastercard", "amex"] + "properties": { "brand": { "enum": ["visa", "mastercard", "amex"] } } } } ], @@ -158,7 +158,7 @@ The response config includes runtime information about what's available for this { "type": "card", "constraints": { - "brands": ["visa", "mastercard", "amex"] + "properties": { "brand": { "enum": ["visa", "mastercard", "amex"] } } } } ], @@ -205,7 +205,7 @@ business's configuration. "id": "processor_tokenizer", "version": "{{ ucp_version }}", "available_instruments": [ - {"type": "card", "constraints": {"brands": ["visa", "mastercard", "amex"]}} + {"type": "card", "constraints": {"properties": {"brand": {"enum": ["visa", "mastercard", "amex"]}}}} ], "config": { "environment": "production", diff --git a/docs/specification/payment/guide.md b/docs/specification/payment/guide.md index e4e9102b0..e37564584 100644 --- a/docs/specification/payment/guide.md +++ b/docs/specification/payment/guide.md @@ -190,23 +190,27 @@ and desired configuration. restrictions on instrument types or constraints — it supports the full set of instrument types defined by its handler schema. When present, it narrows the advertised types and/or applies additional constraints (e.g., limiting card -brands to `["visa", "mastercard"]`). In a Business profile and authoritative +brands to `visa` and `mastercard`). In a Business profile and authoritative response, array order communicates preferred instrument presentation, earliest first. -Each entry narrows along two axes: +Each entry narrows along two axes. Both are +[Constraint Expressions](../overview/index.md#constraint-expression), so one +evaluator runs both; they differ only in the object they describe: ```text available_instruments[] ├── type selects an instrument schema -├── constraints derived values, not submitted (card: brand) -└── ucp.request_constraints requirements on submitted instruments and credentials +├── constraints describes that schema's `constraint_target` +└── ucp.request_constraints describes the request that submits it, via `path` ``` -`constraints` carries values the Business derives rather than receives, so they -cannot be expressed as requirements on request data. The card availability -schema defines `brand`, the set of accepted card schemes, which is derived from -the account number and survives tokenization. +`constraints` admits no `path`. Its object is standing, not selected: the +instrument schema for the entry's `type` declares it as `$defs/constraint_target`, +and keys in `properties` name that object's members. The target holds what the +Business derives rather than receives — for cards, `brand`, read from the account +number and surviving tokenization — which is why those values cannot be expressed +as requirements on request data. `ucp.request_constraints` carries everything that *is* on the wire: which instrument fields are required, which credential types are accepted, and which @@ -236,7 +240,7 @@ schemas is what gives each branch a stable discriminator. "available_instruments": [ { "type": "card", - "constraints": { "brand": { "enum": ["visa", "mastercard"] } }, + "constraints": { "properties": { "brand": { "enum": ["visa", "mastercard"] } } }, "ucp": { "request_constraints": { "path": "$['payment']['instruments'][?@['handler_id'] == 'processor_tokenizer_1234' && @['type'] == 'card']", @@ -291,7 +295,7 @@ and typically includes different configuration: { "type": "card", "constraints": { - "brands": ["visa", "mastercard"] + "properties": { "brand": { "enum": ["visa", "mastercard"] } } } } ], @@ -315,7 +319,7 @@ and typically includes different configuration: { "type": "card", "constraints": { - "brands": ["visa", "mastercard", "amex", "discover"] + "properties": { "brand": { "enum": ["visa", "mastercard", "amex", "discover"] } } } } ], @@ -337,7 +341,7 @@ and typically includes different configuration: { "type": "card", "constraints": { - "brands": ["visa", "mastercard"] + "properties": { "brand": { "enum": ["visa", "mastercard"] } } } } ], @@ -377,9 +381,9 @@ 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", constraints: {properties: {brand: {enum: ["visa", "mastercard", "amex", "discover"]}}}}]` | +| Business profile | `[{type: "card", constraints: {properties: {brand: {enum: ["visa", "mastercard", "amex"]}}}}]` | +| **Response (resolved)** | `[{type: "card", constraints: {properties: {brand: {enum: ["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. @@ -578,28 +582,20 @@ authors **MAY** extend any of the base instruments to add handler-specific display data or customize the credential reference. Handlers **MAY** define multiple instrument types for different payment flows. -**Available Instrument Schemas:** +**Constraint Targets:** -Each instrument schema defines its own `available_*` variant in `$defs` that -specifies what constraints are valid for that instrument type. For example, +An instrument schema declares what its availability constraints may name in +`$defs/constraint_target`: a plain object of members and their types, never +carried in a payload. The handler's `payment_instrument` set binds each `type` +to the schema that owns its target. [`card_payment_instrument.json`](site:schemas/shopping/types/card_payment_instrument.json) -defines `available_card_payment_instrument` with a `brand` constraint. - -| Schema | Constraints | -| :----------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | -| [`available_payment_instrument.json`](site:schemas/shopping/types/available_payment_instrument.json) | Base: type, constraints (open object) | -| `card_payment_instrument.json#/$defs/available_card_payment_instrument` | Extends base with `constraints.brand` for card schemes | - -Handlers reference these instrument-defined schemas when declaring -`available_instruments`. The **instrument schema authors** define what -constraints are meaningful (e.g., `brand` for cards), and **platforms/businesses** use this to advertise what they support (e.g., `{ "enum": ["visa", "mastercard"] }`). - -The base Payment Handler intentionally remains open: an instrument `type` does -not globally select a schema, so a card-specific constraint is only validated -where a handler schema names the card definition. A handler that expects its -`available_instruments` entries to be validated **SHOULD** reference the -instrument's `available_*` definition and declare the optional `ucp` property, -which is what brings `request_constraints` into the validated path. +declares `brand` as a string, so +`{ "properties": { "brand": { "enum": ["visa", "mastercard"] } } }` names a +declared member and pins it to string values. The base Payment Handler validates +the grammar, so a malformed `constraints` fails everywhere; the target supplies +the meaning, so naming an undeclared member or pinning the wrong type is +reportable without the base dispatching on `type`. A handler extending an +instrument **SHOULD** extend that instrument's target rather than restate it. **Example `types/tokenizer_instrument.json`**: @@ -612,25 +608,17 @@ which is what brings `request_constraints` into the validated path. "description": "Card-based payment instrument for com.example.tokenizer.", "$defs": { - "available_tokenizer_card": { - "title": "Available Tokenizer Card", - "description": "Card instrument availability with tokenizer-specific constraints.", + "constraint_target": { + "title": "Tokenizer Card Constraint Target", + "description": "Extends the card target with tokenizer-specific members.", "allOf": [ - { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/types/card_payment_instrument.json#/$defs/available_card_payment_instrument" }, + { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/types/card_payment_instrument.json#/$defs/constraint_target" }, { "type": "object", "properties": { - "type": { "const": "tokenizer_card" }, - "ucp": { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/ucp.json#/$defs/members" }, - "constraints": { - "type": "object", - "properties": { - "tokenization_types": { - "type": "array", - "items": { "type": "string" }, - "description": "Supported tokenization types (e.g., ['network_token', 'merchant_token'])." - } - } + "tokenization_type": { + "type": "string", + "description": "How the credential was tokenized (e.g., 'network_token', 'merchant_token')." } } } diff --git a/docs/specification/shopping/checkout/mcp.md b/docs/specification/shopping/checkout/mcp.md index a511d30d1..34a7f3085 100644 --- a/docs/specification/shopping/checkout/mcp.md +++ b/docs/specification/shopping/checkout/mcp.md @@ -67,7 +67,7 @@ Businesses advertise MCP transport availability through their UCP profile at "spec": "https://example.vendor.com/specs/delegate-payment", "schema": "https://example.vendor.com/schemas/delegate-payment-config.json", "available_instruments": [ - {"type": "card", "constraints": {"brands": ["visa", "mastercard"]}} + {"type": "card", "constraints": {"properties": {"brand": {"enum": ["visa", "mastercard"]}}}} ], "config": {} } diff --git a/source/schemas/shopping/types/available_payment_instrument.json b/source/schemas/shopping/types/available_payment_instrument.json index cf847b355..9bb8b2c76 100644 --- a/source/schemas/shopping/types/available_payment_instrument.json +++ b/source/schemas/shopping/types/available_payment_instrument.json @@ -11,10 +11,8 @@ "description": "The instrument type identifier (e.g., 'card', 'gift_card'). References an instrument schema's type constant." }, "constraints": { - "type": "object", - "additionalProperties": true, - "description": "Constraints on this instrument type. Structure depends on instrument type and active capabilities.", - "minProperties": 1 + "$ref": "../../common/types/constraint_expression.json", + "description": "A Constraint Expression describing the instrument this entry makes available. Keys in `properties` name members of the `constraint_target` declared by the instrument schema for this `type`. Requirements on submitted request data belong in `ucp.request_constraints` instead." } } } diff --git a/source/schemas/shopping/types/card_payment_instrument.json b/source/schemas/shopping/types/card_payment_instrument.json index bf130c14d..099ceba4b 100644 --- a/source/schemas/shopping/types/card_payment_instrument.json +++ b/source/schemas/shopping/types/card_payment_instrument.json @@ -4,67 +4,21 @@ "title": "Card Payment Instrument", "description": "A basic card payment instrument with visible card details. Can be inherited by a handler's instrument schema to define handler-specific display details or more complex credential structures.", "$defs": { - "available_card_payment_instrument": { - "title": "Available Card Payment Instrument", - "description": "Declares card instrument availability with card-specific constraints.", - "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']).", - "deprecated": true - }, - "brand": { - "title": "Card Brand Constraint", - "description": "Card schemes accepted. Derived from the account number, not submitted.", - "allOf": [ - { - "$ref": "../../common/types/constraint_expression.json#/$defs/value_constraint" - } - ], - "properties": { - "enum": { - "items": { - "type": "string" - } - }, - "const": { - "type": "string" - } - }, - "examples": [ - { - "enum": [ - "visa", - "mastercard" - ] - }, - { - "const": "cartebancaire" - } - ] - } - } - } - } + "constraint_target": { + "title": "Card Constraint Target", + "description": "The object an available card instrument's `constraints` describes. It declares the constrainable members and their types and is never carried in a payload.", + "type": "object", + "properties": { + "brand": { + "type": "string", + "description": "Card scheme. Derived from the account number, not submitted.", + "examples": [ + "visa", + "mastercard", + "cartebancaire" + ] } - ] + } } }, "allOf": [ From 6a668e17d1ed5b941653a18e690b5cad7e6d114c Mon Sep 17 00:00:00 2001 From: Daniel Wyckoff Date: Sat, 22 Aug 2026 08:58:09 -0400 Subject: [PATCH 4/4] now fixing the index documentation after merging --- docs/specification/overview/index.md | 36 +++++++++++++++++----------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/docs/specification/overview/index.md b/docs/specification/overview/index.md index d7892375f..edb2fd372 100644 --- a/docs/specification/overview/index.md +++ b/docs/specification/overview/index.md @@ -558,8 +558,10 @@ payment policy. #### Alternative verification requirements on a submitted credential A Business accepts more than one credential shape and requires different -verification data for each. In this example, a raw PAN must carry a `cvc`, and a -network token must carry a `cryptogram` with its `eci_value`: +verification data for each. In this example, a PAN must carry a `cvc`, and a +network token must carry a `cryptogram` with its `eci_value`. Each credential +family is its own schema, so every branch discriminates on the credential's own +`type` and no rule has to branch on a sibling field: ```json @@ -573,12 +575,12 @@ network token must carry a `cryptogram` with its `eci_value`: "credential": { "anyOf": [ { - "properties": {"card_number_type": {"const": "fpan"}}, - "required": ["card_number_type", "cvc"] + "properties": {"type": {"const": "pan"}}, + "required": ["cvc"] }, { - "properties": {"card_number_type": {"const": "network_token"}}, - "required": ["card_number_type", "cryptogram", "eci_value"] + "properties": {"type": {"const": "network_token"}}, + "required": ["cryptogram", "eci_value"] } ] } @@ -591,20 +593,26 @@ network token must carry a `cryptogram` with its `eci_value`: One path selects the submitted instrument, and one Object Constraint describes it. The sibling `required` applies to every matching instrument; the `anyOf` branches then apply to the nested `credential` object, which must satisfy at -least one. Each branch pins `card_number_type` and also names it in `required`, -so a branch matches only the credential shape it describes. A raw PAN without a -`cvc` fails, as does a network token missing its `eci_value`. +least one. Each branch pins `type` with `const`, so a branch matches only the +credential family it describes; +[`payment_credential.json`](site:schemas/shopping/types/payment_credential.json) +already requires `type` on every credential, so no branch has to name it in +`required`. A [PAN credential](site:schemas/shopping/types/pan_credential.json) +without a `cvc` fails, as does a [network +token](site:schemas/shopping/types/network_token_credential.json) missing its +`eci_value`. Because every branch pins the discriminator, the branch set also closes the -accepted values. A `dpan` credential is valid under -[`card_credential.json`](site:schemas/shopping/types/card_credential.json) but -satisfies neither branch, so this Business does not accept it at this path. A -Business that later accepts a new variant adds a branch for it. +accepted credential families. A handler +[token credential](site:schemas/shopping/types/token_credential.json) is a valid +credential at this position but satisfies neither branch, so this Business does +not accept it at this path. A Business that later accepts another family adds a +branch for it. Two separately targeted constraints cannot express this rule. Request Constraints conjoin, so one value requiring `cvc` and another requiring `cryptogram` would require both. Discriminating through the path filter instead -— selecting `fpan` credentials in one value and `network_token` credentials in +— selecting `pan` credentials in one value and `network_token` credentials in another — moves conditional logic into the selector, which paths do not carry. ## Actions