Skip to content

refactor!: Refactor Payment constructs (including related extensions) from shopping/ to common/ - #741

Merged
igrigorik merged 20 commits into
Universal-Commerce-Protocol:mainfrom
jingyli:payment-types
Aug 25, 2026
Merged

refactor!: Refactor Payment constructs (including related extensions) from shopping/ to common/#741
igrigorik merged 20 commits into
Universal-Commerce-Protocol:mainfrom
jingyli:payment-types

Conversation

@jingyli

@jingyli jingyli commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Description

A continuation of #436 , addressing Phase 5 in that PR. Also related to #723 and #736 when it comes to other schema & specification documentation clean-ups.

Design choices made in this PR:

  • Moved 5 core payment constructs and their related types from source/schemas/shopping to source/schemas/common as it is a horizontal concept instead of vertical specific:
    • split_payments.json
    • payment_terms.json
    • payment_authentication.json
    • ap2_mandate.json
  • "Demoted" payment.json to a common type instead of living in the same level as the extensions above.
  • Only exception is to continue leaving binding.json in source/schemas/shopping/types as it explicitly references checkout sessions, which is not vertical agnostic.
  • Also updated all documentation and renamed extension names to live in the dev.ucp.common.payment namespace.
  • Applied the new specification documentation structure to maintain repo consistency.

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)
  • Governance/Contributing: Updates to GOVERNANCE.md, CONTRIBUTING.md, or CODEOWNERS. (Requires Governance Council approval)
  • Capability: New schemas (Discovery, Cart, etc.) or extensions. (Requires Maintainer approval)
  • Documentation: Updates to README, or documentations regarding schema or capabilities. (Requires Maintainer approval)
  • Infrastructure: CI/CD, Linters, or build scripts. (Requires DevOps Maintainer approval)
  • Maintenance: Version bumps, lockfile updates, or minor bug fixes. (Requires DevOps Maintainer approval)
  • SDK: Language-specific SDK updates and releases. (Requires DevOps Maintainer approval)
  • Samples / Conformance: Maintaining samples and the conformance suite. (Requires Maintainer approval)
  • UCP Schema: Changes to the ucp-schema tool (resolver, linter, validator). (Requires Maintainer approval)
  • Community Health (.github): Updates to templates, workflows, or org-level configs. (Requires DevOps Maintainer approval)

Related Issues

Phase 2 in #520

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.

Screenshots / Logs (if applicable)

A view of the new payment nav bar that follows the new rendering & extensions nesting:
payment-nav-bar

@jingyli jingyli added this to the 2026-08-24 milestone Aug 18, 2026
@jingyli jingyli added the TC review Ready for TC review label Aug 18, 2026
@jingyli
jingyli requested a review from prasad-stripe August 18, 2026 17:40
@iantrainor iantrainor added the area:payments Issues and pull requests related to the Payments vertical label Aug 19, 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.

A couple of gotchas to address...

presentation regressions, same flag as #736

Twelve existing payment-field tables collapse to Schema Reference links after their schemas move into common/types. The selected-payment-instrument $defs table also becomes a bare link through extension_schema_fields. Finally, Payment Authentication, Payment Terms, and Split Payments disappear entirely from the generated Extension Schemas reference because the common-root scan excludes extensions and the extension scan only examines Shopping.

The #736 default-inline fix should cover schema_fields; the same default-inline/explicit-opt-out behavior must cover extension_schema_fields:

{{ auto_generate_schema_reference(
    '.',
    'reference',
    include_capability=False,
    base_dir='source/schemas/common'
) }}

That should retain current rendering and visibility.

Incomplete namespace/path migration

Old identities or locations remain in:

  • docs/documentation/core-concepts.md
  • docs/specification/overview.md
  • docs/specification/payment-authentication.md
  • docs/specification/payment-handler-guide.md
  • scripts/scaffolds/common_payment_authentication_response.json
  • docs/documentation/schema-authoring.md

The scaffold is particularly concerning: it was renamed to common_payment_authentication_response.json but still advertises dev.ucp.shopping.payment_authentication. Validation accepts this because capability registries are open.

Comment thread source/schemas/common/split_payments.json Outdated
Comment thread source/schemas/common/split_payments.json Outdated
@damaz91 damaz91 added status:under-review gov:needs-tc-review Requires review and approval from the Technical Council labels Aug 20, 2026

@raginpirate raginpirate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd also want to call out that dev.ucp.payment.three_ds_challenge and dev.ucp.payment.device_data_collection also seem to be left under payment instead of common; is the general stance to re-namespace all of these?

"description": "Extends Checkout with selectable payment terms, letting a Business offer alternative schedules for when payment for the checkout is due, and projects the accepted term onto the resulting Order.",
"description": "Extends capabilities (e.g., checkout in retail shopping) with selectable payment terms, letting a Business offer alternative schedules for when payment for the checkout is due, and projects the accepted term onto the resulting Order.",
"requires": {
"protocol": { "min": "2026-04-08" },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do we need to drop / bump these min protocol requirements on these extensions now that we've re-namespaced them? It doesn't make sense to ref these artifacts against totally old spec versions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think dropping them as starters may be better since there is no concrete date we can anchor against. We probably need a follow-up PR to add them back once the new version is cut?

Comment thread docs/specification/shopping/catalog/rest.md Outdated
@jingyli

jingyli commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @igrigorik and @raginpirate!

presentation regressions, same flag as #736

I ran some manual validations after adding in extension rendering for source/schemas/common/ and found an interesting bug where our current main.py logic is rendering extension elements that have no properties defined. It was fixed in 7b38d05 and the effects are as follow:

Before:

reference-before

After:

reference-after

Incomplete namespace/path migration

I did a pass over the affected files and updated the stale links. One interesting callout is that #723 only consolidated some payment extensions into the payment folder (e.g., payment_authentication) so as part of this PR, we further moved extensions like split_payments and ap2_mandates into the payment/ subfolder. Also the original PR missed including ap2_mandates schema refactoring, which is also fixed now.

@raginpirate's comment:

I'd also want to call out that dev.ucp.payment.three_ds_challenge and dev.ucp.payment.device_data_collection also seem to be left under payment instead of common; is the general stance to re-namespace all of these?

Per deep-dive discussion, we do not want to accidentally frame payment as a service in UCP and therefore will follow a consistent pattern where common remains the service associated with these payment authentication methods:

  • dev.ucp.payment.three_ds_challenge -> dev.ucp.common.payment.three_ds_challenge
  • dev.ucp.payment.device_data_collection -> dev.ucp.common.payment.device_data_collection

Comment thread source/schemas/common/types/payment.json

@raginpirate raginpirate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As of right now, this PR lands only some payments things as dev.ucp.common.payments and others as just dev.ucp.common.

I'd love to know if we can land this as a unified answer across the board, here's a quick commit my agent threw together... anything off with this direction?
52a6967

prasad-stripe added a commit that referenced this pull request Aug 21, 2026
PR #741 moves the payment constructs to `common/` but keeps
`binding.json` in `shopping/types` because it references checkout
sessions. That leaves the tokenization handler contract, which lives in
`source/handlers/` and is horizontal by construction, requiring a
`checkout_id` on a required field of both `/tokenize` and `/detokenize`.
A non-shopping capability cannot call either endpoint without
fabricating a checkout identifier. The same file already refs the
credential from `common/`, so the asymmetry is only in binding.

Binding becomes a flat `{type, id}` pair in `common/types/binding.json`:

- `type` is the capability that owns the bound resource, refs
  `reverse_domain_name.json`, e.g. `dev.ucp.shopping.checkout`.
- `id` is the opaque identifier of that resource within the capability.
- `identity` is unchanged.
- `additionalProperties: false` makes exact-equality verification
  decidable.

The tokenization handler refs this schema, exactly as it already refs
the base credential. One binding shape covers every capability, so no
per-vertical binding schema is needed and SDKs keep a single model.

Tokenizers need no new logic. Binding was never semantically validated
(a tokenizer cannot confirm a checkout exists), so it is an opaque
equality token. The guide now states three normative rules that make
this explicit: verification is exact equality over the whole binding
object, `id` is opaque and MUST NOT be parsed or resolved, and a
tokenizer MUST NOT reject a request solely because it does not
recognize `type`. Those rules are what close the cross-type confusion
this generalization would otherwise introduce.

BREAKING CHANGE: `https://ucp.dev/schemas/shopping/types/binding.json`
moves to `https://ucp.dev/schemas/common/types/binding.json`, and
`checkout_id` is replaced by `type` plus `id`. Checkout bindings become
`{"type": "dev.ucp.shopping.checkout", "id": "<checkout_id>"}`. Stacked
on #741 so implementers absorb both moves in one release.

Binding shape credit to @gsmith85, who proposed the flat form in
review of the polymorphic base-plus-subtype version.
prasad-stripe added a commit that referenced this pull request Aug 21, 2026
PR #741 moves the payment constructs to `common/` but keeps
`binding.json` in `shopping/types` because it references checkout
sessions. That leaves the tokenization handler contract, which lives in
`source/handlers/` and is horizontal by construction, requiring a
`checkout_id` on a required field of both `/tokenize` and `/detokenize`.
A non-shopping capability cannot call either endpoint without
fabricating a checkout identifier. The same file already refs the
credential from `common/`, so the asymmetry is only in binding.

Binding becomes a flat `{type, id}` pair in `common/types/binding.json`:

- `type` is the capability that owns the bound resource, refs
  `reverse_domain_name.json`, e.g. `dev.ucp.shopping.checkout`.
- `id` is the opaque identifier of that resource within the capability.

Resource scope and participant scope are now separate. `identity` moves
out of `binding` to a sibling field on the `/tokenize` and `/detokenize`
request bodies: `binding` says which resource the token is for,
`identity` says which participant it is for. `payment_identity.json`
itself is unchanged; only its position in the request moves.

The tokenization handler refs both schemas, exactly as it already refs
the base credential. One binding shape covers every capability, so no
per-vertical binding schema is needed and SDKs keep a single model.

Tokenizers need no new logic. Binding was never semantically validated
(a tokenizer cannot confirm a checkout exists), so it is an opaque
equality token. The guide now states three normative rules that make
this explicit: verification is exact equality over the binding object
and the identity presented with it, `binding.id` is opaque and MUST NOT
be parsed or resolved, and a tokenizer MUST NOT reject a request solely
because it does not recognize `binding.type`. Those rules are what close
the cross-type confusion this generalization would otherwise introduce.

BREAKING CHANGE: `https://ucp.dev/schemas/shopping/types/binding.json`
moves to `https://ucp.dev/schemas/common/types/binding.json`,
`checkout_id` is replaced by `type` plus `id`, and `identity` moves from
inside `binding` to a sibling request field. Checkout bindings become
`{"type": "dev.ucp.shopping.checkout", "id": "<checkout_id>"}`. Stacked
on #741 so implementers absorb both moves in one release.

Binding shape credit to @gsmith85, who proposed the flat form in review.
Identity separation credit to @raginpirate.
prasad-stripe added a commit that referenced this pull request Aug 21, 2026
PR #741 moves the payment constructs to `common/` but keeps
`binding.json` in `shopping/types` because it references checkout
sessions. That leaves the tokenization handler contract, which lives in
`source/handlers/` and is horizontal by construction, requiring a
`checkout_id` on a required field of both `/tokenize` and `/detokenize`.
A non-shopping capability cannot call either endpoint without
fabricating a checkout identifier. The same file already refs the
credential from `common/`, so the asymmetry is only in binding.

Binding becomes a flat `{type, id}` pair in `common/types/binding.json`:

- `type` is the capability that owns the bound resource, refs
  `reverse_domain_name.json`, e.g. `dev.ucp.shopping.checkout`.
- `id` is the opaque identifier of that resource within the capability.

Resource scope and participant scope are now separate. `identity` moves
out of `binding` to a sibling field on the `/tokenize` and `/detokenize`
request bodies: `binding` says which resource the token is for,
`identity` says which participant it is for. `payment_identity.json`
itself is unchanged; only its position in the request moves.

The tokenization handler refs both schemas, exactly as it already refs
the base credential. One binding shape covers every capability, so no
per-vertical binding schema is needed and SDKs keep a single model.

Tokenizers need no new logic. Binding was never semantically validated
(a tokenizer cannot confirm a checkout exists), so it is an opaque
equality token. The guide now states three normative rules that make
this explicit: verification is exact equality over the binding object
and the identity presented with it, `binding.id` is opaque and MUST NOT
be parsed or resolved, and a tokenizer MUST NOT reject a request solely
because it does not recognize `binding.type`. Those rules are what close
the cross-type confusion this generalization would otherwise introduce.

BREAKING CHANGE: `https://ucp.dev/schemas/shopping/types/binding.json`
moves to `https://ucp.dev/schemas/common/types/binding.json`,
`checkout_id` is replaced by `type` plus `id`, and `identity` moves from
inside `binding` to a sibling request field. Checkout bindings become
`{"type": "dev.ucp.shopping.checkout", "id": "<checkout_id>"}`. Stacked
on #741 so implementers absorb both moves in one release.

Binding shape credit to @gsmith85, who proposed the flat form in review.
Identity separation credit to @raginpirate.
@jingyli

jingyli commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

@raginpirate thanks for the callout! Was a miss on my end to misinterpret the dev.ucp.common.payment namespace to only be applicable for the authentication actions.

Now have updated all payment relevant constructs in this namespace for consistency. PTAL!

@jingyli
jingyli requested a review from raginpirate August 22, 2026 02:13

@raginpirate raginpirate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for iterating on this @jingyli, I'm glad we've converged on a direction.
My agent spotted a few more misses across the docs; mind cherry-picking these in?
f62be4f

prasad-stripe added a commit that referenced this pull request Aug 24, 2026
PR #741 moves the payment constructs to `common/` but keeps
`binding.json` in `shopping/types` because it references checkout
sessions. That leaves the tokenization handler contract, which lives in
`source/handlers/` and is horizontal by construction, requiring a
`checkout_id` on a required field of both `/tokenize` and `/detokenize`.
A non-shopping capability cannot call either endpoint without
fabricating a checkout identifier. The same file already refs the
credential from `common/`, so the asymmetry is only in binding.

Binding becomes a flat `{type, id}` pair in `common/types/binding.json`:

- `type` is the capability that owns the bound resource, refs
  `reverse_domain_name.json`, e.g. `dev.ucp.shopping.checkout`.
- `id` is the opaque identifier of that resource within the capability.

Resource scope and participant scope are now separate. `identity` moves
out of `binding` to a sibling field on the `/tokenize` and `/detokenize`
request bodies: `binding` says which resource the token is for,
`identity` says which participant it is for. `payment_identity.json`
itself is unchanged; only its position in the request moves.

The tokenization handler refs both schemas, exactly as it already refs
the base credential. One binding shape covers every capability, so no
per-vertical binding schema is needed and SDKs keep a single model.

Tokenizers need no new logic. Binding was never semantically validated
(a tokenizer cannot confirm a checkout exists), so it is an opaque
equality token. The guide now states three normative rules that make
this explicit: verification is exact equality over the binding object
and the identity presented with it, `binding.id` is opaque and MUST NOT
be parsed or resolved, and a tokenizer MUST NOT reject a request solely
because it does not recognize `binding.type`. Those rules are what close
the cross-type confusion this generalization would otherwise introduce.

BREAKING CHANGE: `https://ucp.dev/schemas/shopping/types/binding.json`
moves to `https://ucp.dev/schemas/common/types/binding.json`,
`checkout_id` is replaced by `type` plus `id`, and `identity` moves from
inside `binding` to a sibling request field. Checkout bindings become
`{"type": "dev.ucp.shopping.checkout", "id": "<checkout_id>"}`. Requesting
this rides #741's release so implementers absorb both moves at once.

Binding shape credit to @gsmith85, who proposed the flat form in review.
Identity separation credit to @raginpirate.
@jingyli

jingyli commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

@raginpirate Good catch via f62be4f, I've reconciled them into the PR and also merged in #424's scope to this refactoring (added the 2 additional files being moved to common/types - network_token_credential.json and pan_credential.json) in 8832cc7

@jingyli
jingyli requested a review from raginpirate August 24, 2026 18:09

@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.

Two nits, otherwise LGTM.

Comment thread docs/specification/overview/index.md Outdated
Comment thread source/schemas/common/payment_split_payments.json Outdated
prasad-stripe added a commit that referenced this pull request Aug 24, 2026
* refactor!: make token binding vertical-agnostic

PR #741 moves the payment constructs to `common/` but keeps
`binding.json` in `shopping/types` because it references checkout
sessions. That leaves the tokenization handler contract, which lives in
`source/handlers/` and is horizontal by construction, requiring a
`checkout_id` on a required field of both `/tokenize` and `/detokenize`.
A non-shopping capability cannot call either endpoint without
fabricating a checkout identifier. The same file already refs the
credential from `common/`, so the asymmetry is only in binding.

Binding becomes a flat `{type, id}` pair in `common/types/binding.json`:

- `type` is the capability that owns the bound resource, refs
  `reverse_domain_name.json`, e.g. `dev.ucp.shopping.checkout`.
- `id` is the opaque identifier of that resource within the capability.

Resource scope and participant scope are now separate. `identity` moves
out of `binding` to a sibling field on the `/tokenize` and `/detokenize`
request bodies: `binding` says which resource the token is for,
`identity` says which participant it is for. `payment_identity.json`
itself is unchanged; only its position in the request moves.

The tokenization handler refs both schemas, exactly as it already refs
the base credential. One binding shape covers every capability, so no
per-vertical binding schema is needed and SDKs keep a single model.

Tokenizers need no new logic. Binding was never semantically validated
(a tokenizer cannot confirm a checkout exists), so it is an opaque
equality token. The guide now states three normative rules that make
this explicit: verification is exact equality over the binding object
and the identity presented with it, `binding.id` is opaque and MUST NOT
be parsed or resolved, and a tokenizer MUST NOT reject a request solely
because it does not recognize `binding.type`. Those rules are what close
the cross-type confusion this generalization would otherwise introduce.

BREAKING CHANGE: `https://ucp.dev/schemas/shopping/types/binding.json`
moves to `https://ucp.dev/schemas/common/types/binding.json`,
`checkout_id` is replaced by `type` plus `id`, and `identity` moves from
inside `binding` to a sibling request field. Checkout bindings become
`{"type": "dev.ucp.shopping.checkout", "id": "<checkout_id>"}`. Requesting
this rides #741's release so implementers absorb both moves at once.

Binding shape credit to @gsmith85, who proposed the flat form in review.
Identity separation credit to @raginpirate.

* fix: separate resource binding from participant authorization

   A token is bound to a resource and issued to a participant. Those need
   different checks — the resource is a replay guard compared for exact
   equality, the participant is an authorization question.

   #746 moves `identity` out of the `binding` object but rule 1 still
   compares it. That can't work, because `identity` depends on who is
   calling: a business omits it when detokenizing directly, its PSP
   includes it when acting on the business's behalf (platform-tokenizer
   L288 and L487). Same token, two legal shapes, so equality over it
   passes at most one of them.

   - Rule 1 compares `type` and `id` only, and ignores unrecognized
     members rather than rejecting or comparing them. `binding` is an
     open object now and nothing else defines this.
   - Rule 2: a tokenizer must not *depend* on `binding.id` resolving,
     but may check locally if it owns the resource.
   - Rule 4 (new): record the participant at mint, verify it at burn.
   - `/detokenize` told callers to omit `identity` "when it was omitted
     at tokenization"; the prose says omit it when you are the target.
     Those disagree — fixed to match the prose.
   - `minLength: 1` on `binding.id`.

* docs: align binding vocabulary and tighten tokenizer rules

Follow-ups to #762 across the payment specification.

The payment guide still described binding as an association to a
checkout, in the Key Definitions entry, the instrument acquisition input
table, the handler authoring guidance, and the security best practice.
Binding is to a capability resource identified by `type` and `id`. The
definition also folded business identity into binding, which is now the
separate participant axis.

The platform tokenizer example needed the same split. Binding
verification now names the requesting participant rather than "caller
identity", and the requirement labelled "Identity binding" becomes
"Issued to participant". The row below it, "Resource-bound", is then the
only one using bound, which is correct.

Two tokenizer rules are adjusted from #762. Rule 1 no longer declares
that members other than `type` and `id` fall outside the replay guard: a
Tokenizer MUST NOT reject unrecognized members and MUST ignore them when
comparing, but MAY compare members defined by an extension it
implements, so an extension that scopes a binding can still mean
something. Rule 4 now states that authority for one participant to act
for another is handler-defined and outside this specification, which the
MUST otherwise leaves ungrounded.

Markdown tables reflowed to the file convention where new text changed
column widths.

Rule adjustments in response to review by @igrigorik.

* Update docs/specification/payment/tokenization.md

Co-authored-by: Ilya Grigorik <ilya@grigorik.com>

* docs: use generic binding vocabulary in encrypted credential example

The encrypted credential handler embeds binding inside its own encrypted
payload rather than calling /tokenize or /detokenize, but the payload is
still carrying the shopping-bound checkout_id. Align it with the binding
placement guidance in template.md, which now says the binding object is
what belongs inside the credential payload.

Reported by @amithanda.

---------

Co-authored-by: Ilya Grigorik <ilya@grigorik.com>
…btree

Moving the payment extension docs into payment/extensions/ left four
already-published URLs with no file and no redirect entry, so they would
have started returning 404 on deploy:

  /specification/payment/authentication/
  /specification/payment/split-payments/
  /specification/payment/actions/device-data-collection/
  /specification/payment/actions/three-ds-challenge/

The existing redirect entries were rewritten to point at the new targets,
which preserved the pre-vertical flat URLs (payment-authentication.md,
split-payments.md, payment-actions/*.md) but skipped the specification/payment/
generation introduced by the vertical reorganization. Those are the paths
currently served on ucp.dev, so they are the ones a live deep link or a
published `spec` URL is most likely to use.

The link checker cannot catch this class of break: it validates outbound
links, and every internal reference was correctly updated, so nothing points
at the dropped paths anymore. URL continuity for already-published addresses
is only visible by diffing the redirect map against the deployed URL set.

@raginpirate raginpirate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the great work! 🚀

@igrigorik
igrigorik merged commit 0b9d7c4 into Universal-Commerce-Protocol:main Aug 25, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:payments Issues and pull requests related to the Payments vertical gov:needs-tc-review Requires review and approval from the Technical Council status:under-review TC review Ready for TC review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants