Skip to content

docs: document business-owned response values#519

Open
karangoel16 wants to merge 4 commits into
Universal-Commerce-Protocol:mainfrom
karangoel16:feat/saved-payment-instruments-extension
Open

docs: document business-owned response values#519
karangoel16 wants to merge 4 commits into
Universal-Commerce-Protocol:mainfrom
karangoel16:feat/saved-payment-instruments-extension

Conversation

@karangoel16

@karangoel16 karangoel16 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Motivation

Review feedback clarified that saved instruments are already possible to surface through existing UCP response entities, especially payment.instruments[]. The gap is that UCP does not clearly document the mechanic of businesses returning business-owned user state, rather than only echoing values supplied by the platform.

This PR now repurposes the original saved-instruments proposal into a documentation clarification:

  • businesses may populate existing response fields from authenticated user state
  • saved payment instruments should use existing payment.instruments[]
  • buyer and fulfillment data follow the same general pattern where the base capability supports them
  • no new dev.ucp.shopping.saved_instruments extension is introduced

Proposal

Add a new docs page, Business-Owned Response Values, explaining that UCP response objects may include values supplied by the business from its own user state when the request is user-authenticated through Identity Linking.

The page includes:

  • platform authentication vs. user authorization framing
  • guidance for saved payment instruments in payment.instruments[]
  • a sample signed, user-authenticated checkout request
  • a sample checkout response with a display-safe saved Shop Pay instrument
  • implementation guidance for opaque business-scoped IDs, ownership checks, and data minimization

Key design choices

  • Use existing base entities. Saved instruments are represented in payment.instruments[], not a new payment.saved_instruments[] field.
  • Docs-only. This PR removes the proposed saved-instruments extension schema and does not add new JSON schema fields.
  • Identity-linked response pattern. The business returns user-owned values only when the request carries appropriate user authorization.
  • Opaque business-owned references. Returned IDs are scoped to the business and resolved by the business server-side.
  • No generic tokenized-node abstraction yet. The broader abstraction for marking any tree node as a tokenized/reference value is left for a future proposal.

What this PR ships

  • docs/specification/business-owned-response-values.md — new docs page for business-owned response values
  • docs/specification/checkout.md — payment section links to the new guidance
  • mkdocs.yml — nav/search entry for the new page
  • Removes the previously proposed docs/specification/saved-instruments.md
  • Removes the previously proposed source/schemas/shopping/saved_instruments.json

Sample shape

Business returns a saved payment method through existing payment.instruments[]:

{
  "payment": {
    "instruments": [
      {
        "id": "saved_shop_pay_4242",
        "handler_id": "shop_pay_1234",
        "type": "shop_pay",
        "selected": true,
        "display": {
          "description": "Saved Shop Pay account",
          "email": "buyer@example.com",
          "last_digits": "4242"
        }
      }
    ]
  }
}

The id is an opaque business-owned reference for the authenticated user. The platform renders/selects it; the business verifies ownership and resolves it server-side.

Follow-ups intentionally not in this PR

  • A generic reference/tokenized-node abstraction for marking any response node as an opaque business-owned reference.
  • Standard metadata for saved/default/managed lifecycle state across response entities.
  • Saved addresses or saved instrument management APIs.

Category (Required)

  • Documentation: Updates to README, or documentations regarding schema or capabilities.

Checklist

  • I have followed the Contributing Guide (Conventional Commits title, no breaking changes).
  • I have updated the documentation.
  • My changes pass all local linting and formatting checks. I ran whitespace validation via git diff --check; I did not run the full docs build locally.
  • I have added tests that prove my fix is effective or that my feature works. Docs-only clarification; no behavior tests applicable.
  • (For Core/Capability) I have included/updated the relevant JSON schemas. No schema change in the repurposed PR.

Discussion welcome on

  1. Whether the new page belongs under Checkout Capability or a more general Core/Overview section.
  2. Whether the sample should use Shop Pay, card, or a more generic handler.
  3. Whether to add a short note to Cart and Fulfillment docs now, or keep this PR focused on checkout/payment examples.
  4. How to scope the follow-up generic reference/tokenized-node abstraction.

@google-cla

google-cla Bot commented Jun 15, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@karangoel16
karangoel16 force-pushed the feat/saved-payment-instruments-extension branch from 87da12d to be1335c Compare June 16, 2026 00:26
…_instruments)

Adds a response-only extension that surfaces the authenticated user's
saved payment instruments on cart and checkout responses under
`payment.saved_instruments[]`. Identity-gated by a new
`dev.ucp.shopping.saved_instruments:read` scope listed in
`identity_linking.config.scopes`. Mirrors the loyalty extension pattern.

Key design choices:
- Response-only (`ucp_request: omit`) — flows merchant → platform only
- No credential on the wire: schema enforces `"credential": { "not": {} }`.
  Platforms select via `payment.instruments[].id`; the business mints a
  checkout-bound TokenCredential per its declared payment handler.
- Per-instrument `default: true` flag instead of inferring from list order
- Business-scoped IDs; cross-merchant portability explicitly out of scope
@karangoel16
karangoel16 force-pushed the feat/saved-payment-instruments-extension branch from be1335c to 3ff52fe Compare June 16, 2026 01:10
Comment thread docs/specification/saved-instruments.md Outdated
* **Expiration awareness:** Surface "your saved card expires next month"
inline with cart/checkout.

A standalone capability that allows enumeration and management of saved

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.

will remove this

@karangoel16

Copy link
Copy Markdown
Contributor Author

Hi @raginpirate, @amithanda ,

wanted your opinion on the following, since I saw that you both have been actively contributing in this area of identity linking :)

cc: @jamesandersen

@jamesandersen

jamesandersen commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

@karangoel16 thanks for raising this PR proposing a way for users to tap into one of the key benefits of "logging into" the business - accessing saved payment methods to reduce checkout friction! This is directly referenced as one of the possible benefits ("saved addresses and credentials") of the identity linking capability and #358 also raises a very similar proposal.

With the recent identity linking updates in #354 landed, it feels like a good time to dig into the details for saved payment methods ... and potentially other checkout data as the docs allude to.

A few initial comments here:

  • This PR proposes a new dev.ucp.shopping.saved_instruments extension for checkout. However, I'm curious if we can simply re-use the ucp.payment.instruments. In fact, while the docs aren't real clear on this yet, the create checkout REST example does this very thing: returns a shop_pay instrument related to the shop pay handler
    • @raginpirate may be the best to confirm if this is an intended use of ucp.payment.instruments e.g. surface business-saved instruments to the platform for user selection and not only echo back details of platform-supplied instruments
    • If this is the right approach, it may warrant some discussion about some kind of standard payment handler / instrument schema for saved payment methods that can be reused across businesses so each business doesn't need to create a new handler just to represent an abstraction of a payment method it already has on file for which the only work on the platform side is presenting instances to the user for selection.
  • @amithanda / @igrigorik for the buyer contact info ... it seems that - subject to a dev.ucp.shopping.checkout:read scope via identity-linking - the business could populate ucp.buyer from it's own persisted buyer data on a create checkout response. Thoughts?
    • Saved shipping addresses (ucp.fulfillment.methods) feel potentially more tricky ...and probably best handled in a separate PR

@amithanda amithanda added the TC review Ready for TC review label Jun 26, 2026
@raginpirate

Copy link
Copy Markdown
Contributor

👍 You've got that exactly right @jamesandersen, saved instruments are already possible to surface back through UCP and that is intentional: same pattern for fulfillment, buyer, really all base entities of the protocol.

And you bring up a secondary point about specifically how we consider simplifying saved entities in the UCP spec when the platform really does not require knowing how to handle the spec of the instrument to use it (its already got its credential). I don't think some saved instrument payment handler is the right direction: I think theres actually a greater, and simpler, abstraction that UCP is missing to denote any node in the tree as "tokenized" (a lot like our tokenized credential) and drop-in an id + description of what it is when the platform doesn't need to know its full spec context. @rosew, our lead on payments platform at Shopify, was actually working on an internal proposal of this problem as it matters a lot for us thinking about PII shared for these kinds of stored entities. Lets see if we can accelerate that!

In the meantime, I do think this PR highlights that in general UCP does not document the mechanic of surfacing business-owned values rather than simply what the platform supplies. It could be worth repurposing this first PR to tackle that gap!

@karangoel16 karangoel16 changed the title feat: add Saved Payment Instruments extension (dev.ucp.shopping.saved_instruments) docs: document business-owned response values Jun 26, 2026
@karangoel16
karangoel16 force-pushed the feat/saved-payment-instruments-extension branch from 7854804 to dea0d34 Compare June 26, 2026 19:33
@karangoel16

karangoel16 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

@jamesandersen @raginpirate one protocol improvement surfaced while looking at the saved payment instrument example:

Today payment.instruments[] can represent both credential-bearing instruments and display-safe references to business-owned saved instruments. A platform can infer some state from whether credential is present, but it cannot reliably distinguish all cases in a machine-readable way:

  • saved business-owned reference: no credential, id resolves server-side
  • selected instrument that still needs credential acquisition: no credential yet
  • instrument with an attached credential/token: credential present

It may be worth adding an explicit enum in a future protocol revision, for example:

"credential_state": "reference"

with values like:

["reference", "required", "present"]

That would make the saved instrument case clear without relying on opaque id naming like saved_shop_pay_4242, and it would let platforms render/route payment flows more deterministically.

Please do let me know thoughts on the same ?

@jamesandersen

Copy link
Copy Markdown
Contributor

make the saved instrument case clear without relying on opaque id naming like saved_shop_pay_4242

Yep, trying to infer saved state from an id alone would certainly be a bad idea. Something explicit like a credential_state makes a lot more sense to help platforms distinguish from instruments already available vs those they may initiate per the payment handler configuration.

a greater, and simpler, abstraction that UCP is missing to denote any node in the tree as "tokenized"

...however, if I'm tracking @raginpirate accurately, I think he's prompting something that would be a bit more generic and thus applicable not only to credentials but also to other checkout PII that the business could surface to the platform in an identity-linked session

@rosew, our lead on payments platform at Shopify, was actually working on an internal proposal of this problem as it matters a lot for us thinking about PII shared for these kinds of stored entities. Lets see if we can accelerate that!

I look forward to this!

@amithanda

Copy link
Copy Markdown
Contributor

Thanks for incorporating some of the feedback @karangoel16, and +1 to the direction @raginpirate and @jamesandersen steered toward: surfacing business-owned user state through existing entities rather than a new extension. Saved instruments, buyer profile, membership id etc are all valid returns when the session is identity-linked, so getting this documented is worthwhile.

One thing worth settling before we add a new page: is the mechanic actually undocumented, or is it under-anchored? the "business returns its own user state on a response" pattern is arguably already expressed in three places:

  1. Structurally, via ucp_request: "omit". That annotation already means "business-owned response value: the platform MUST NOT send it, the business computes and returns it." It is pervasive today: checkout.status, checkout.currency, totals, messages, links, expires_at, continue_url, order, plus discount, the fulfillment method/option/group types, and order.json.

  2. As a worked precedent, via Loyalty. common/loyalty.json already returns business-owned user state (membership, tier, balances, rewards, provisional/verified status), keyed by reverse-domain, annotated ucp_request: "omit", and unlocked by identity linking. The membership-id case is structurally the same as saved instruments.

  3. For payment specifically, it already works. payment.instruments carries no ucp_request annotation, so it is bidirectional by default, and the base payment_instrument requires only id, handler_id, type (credential is optional). The create-checkout REST example already returns a credential-less shop_pay instrument with selected: true and a display block on a response (docs/specification/checkout-rest.md), which is exactly the saved-instrument shape proposed here.

On the auth framing: identity-linking.md already defines Public / Agent-authenticated / User-authenticated access levels, names "saved addresses, full order history, personalized pricing" as the user-authenticated examples, and gates them through config.scopes (e.g. dev.ucp.shopping.checkout:read). So "platform authentication vs. user authorization" may already have a home there, tied to concrete scope names.

Standalone page v/s clarifying documentation in existing pages
The genuine gap is discoverability. The insight "response fields can carry business-owned user state, not just echoes of platform input" is real, and today it is implicit, spread across the ucp_request annotations, the loyalty extension, and identity-linking. An implementer is unlikely to connect those three on their own. That is worth fixing.

The concern with a standalone concept page is drift: it restates the auth model in looser language than identity-linking's scopes, and it becomes a second source of truth that can diverge from the annotations that actually govern behavior. (Related to your open question 1: the nav currently slots it under Checkout among the extensions, next to Loyalty and Split Payments, which reads as "this is an extension" when it is a cross-cutting clarification.)

Would it make sense to land the same intent as two smaller edits anchored to the existing sources of truth, and drop the standalone page?

1. In identity-linking.md, could we add a short subsection right after the Access Levels section (before ### UCP and OAuth) that names the pattern where the auth model already lives?

### Business-Populated Response Values

When a request is user-authenticated, a business MAY populate response fields it
owns for that user from its own stored state, rather than only echoing values the
platform supplied. Two kinds of fields support this:

* Fields annotated `ucp_request: "omit"` are business-populated by definition:
    the platform does not send them and the business returns them (for example
    `loyalty`, `totals`, `messages`).
* Fields the platform MAY also supply (annotated `optional`, or unannotated) MAY
    additionally be populated by the business from stored user state on the
    response (for example `buyer` and `payment.instruments[]`).

Identity linking is what makes these values user-specific rather than generic.
Subject to the scopes that gate the operation, a business MAY return the user's
loyalty membership in `loyalty` (see [Loyalty](loyalty.md)), saved payment
instruments in `payment.instruments[]` (see [Checkout](checkout.md)), or buyer
profile data in `buyer`.

Businesses MUST only return values that belong to the authenticated user and are
appropriate for the current transaction. Platforms MUST treat returned
business-owned identifiers as opaque and business-scoped, and MUST NOT infer raw
credentials from a display value. Businesses SHOULD omit this state when the
request lacks the required user authorization.

2. In checkout.md, could we keep the pointer you already added in the payment section but retarget it from the removed page to Identity Linking plus the existing REST example, so the saved-instrument guidance sits next to a living example? Replacing the paragraph you added with:

Businesses may also populate `payment.instruments[]` on responses with
display-safe payment instruments from their own user state, such as payment
methods the authenticated buyer has saved with the business. Such an instrument
carries no raw credential: it is a display-safe, business-scoped reference (an
opaque `id` plus `display` fields) that the business resolves server-side when
the buyer selects it. The
[create checkout REST example](checkout-rest.md#create-checkout) already returns
this shape for a Shop Pay instrument. Returning user-specific saved state
requires a user-authenticated request; see
[Identity Linking](identity-linking.md) for the access levels and scopes that
gate it.

The `display` fields let the platform present a saved instrument and let the
buyer recognize it. Whether the instrument can be charged as-is or still
requires credential collection is not distinguished by the base schema today, so
platforms should not infer readiness from the absence of a `credential` or from
`id` naming.

Together these two edits deliver the clarification you set out to add (a business may surface its own user state, including saved instruments and membership, on identity-linked responses) while keeping the auth framing and the payment example anchored to the specs that already own them.

On the follow-ups, strong agree they are separate from this docs change and I like that you have them listed as out of scope. Worth flagging that the saved-instrument example motivates one of them directly: display metadata already covers presentation and buyer recognition, but nothing lets the platform deterministically tell a ready-to-resolve saved reference from an instrument that still needs credential collection (credential absence is ambiguous between the two, and id naming is the signal we already agreed not to lean on). @raginpirate's generic tokenized-node abstraction feels like it deserves its own enhancement proposal given the PII implications.

Net: the use cases are clearly valid and the clarification is worth shipping. The main question is whether we anchor it to ucp_request + identity-linking (two small edits) instead of introducing a parallel page. Curious what you and @jamesandersen think.

@jamesandersen

jamesandersen commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Thanks @amithanda ! I think extending/clarifying the existing documentation a bit here as discussed would be a quick win for this PR discussion. @karangoel16 I'd second Amit's proposal here.

There may be a minor tweak or two in addition to the docs. For example: payment_instrument.json describes the id property of an instrument instance as "A unique identifier for this instrument instance, assigned by the platform." However, a business will be assigning this value in the case of an identity-linked, saved payment method.

The further enhancements that @raginpirate and @rosew alluded too can improve protocol level clarity of saved (tokenized) nodes. However, simply getting these clarifications in place unlocks/clarifies significant value for both businesses and platforms seeking deliver low friction checkout for identity-linked users.

metadata under `display`.

The response should not include raw payment credentials unless the active
payment handler explicitly requires credential material on that surface. For

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.

Are we aware of any such examples? This would run counter to UCP best practices for credentials. As we make any docs updates I'd suggest leaving this out.

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.

+1. @karangoel16 we should not be condoning transmission of unencrypted credentials at this layer.

@damaz91 damaz91 added the documentation Improvements or additions to documentation label Jul 14, 2026
@jamesandersen

Copy link
Copy Markdown
Contributor

@karangoel16 to make sure these clarifications land in the docs ahead of the next UCP release (short runway on that), I've put up #585 as a draft — a docs-only take that applies the direction we converged on in this thread: no new extension and no standalone page, just anchoring the "a business may populate response fields from its own stored user state, not only echo what the platform supplied" pattern where the auth model already lives.

It folds in the specific edits discussed here:

  • a Business-Populated Response Values subsection in identity-linking.md, following @amithanda's proposal to anchor it in ucp_request + the access levels rather than a parallel page
  • a pointer from the checkout Payments section to the existing create-checkout REST example + Identity Linking
  • a fix to payment_instrument.id's description (it currently reads "assigned by the platform," which isn't accurate for a business-owned saved instrument)
  • a small cross-link from loyalty.md so the membership case is framed as the same pattern

This is very much building on your PR and the discussion here — happy to co-author or land it whatever way is easiest for you. The tokenized-node abstraction (@raginpirate / @rosew) and the ready-vs-needs-credential distinction (your credential_state idea) are left as follow-ups.

@damaz91 damaz91 added status:needs-triage Signal that the PR is ready for human triage area:payments and removed status:needs-triage Signal that the PR is ready for human triage payments labels Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:payments documentation Improvements or additions to documentation TC review Ready for TC review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants