Skip to content

[Proposal]: Clarify processor-tokenizer participant identifier and rename binding.identity.access_token #394

@sbeashwar

Description

@sbeashwar

Summary

Two related issues with how binding.identity is specified for
processor-tokenizer (and other tokenization) handlers:

  1. The processor-tokenizer example doc describes the
    binding.identity.access_token field as a "Merchant Secure
    Identifier" and the payment_identity.json schema reinforces this
    with "Unique identifier for this participant, obtained during
    onboarding with the tokenizer." Both framings push implementations
    toward treating this value as a confidential bearer credential.

  2. The field name itself - access_token - reinforces the wrong
    mental model. Across OAuth, OpenID, and the rest of UCP (see
    identity-linking.md, where access_token is the OAuth bearer
    credential), access_token denotes a secret that the holder
    presents to authenticate. In binding.identity, the value is the
    opposite: an identifier of the participant the token is being
    bound to, asserted by an already-authenticated caller.

In practice, several PSP processor-tokenization integrations gate
tokenization to a vetted set of caller (agent platform) partners and
authenticate those callers via their own Authorization credential.
Under that model the participant identifier is purely a routing
value
- it tells the tokenizer which participant context to apply -
and does not need to be secret. Treating it as a secret forces an
unnecessary out-of-band issuance flow and prevents the identifier
from being surfaced in the merchant's /.well-known/ucp discovery
profile, where it is the natural place to publish.

This proposal:

  • Names two security models for tokenization handlers (confidential
    vs public participant identifier).
  • Renames binding.identity.access_token to
    binding.identity.participant_id to match the existing schema
    description ("Identity of a participant") and remove the
    bearer-credential connotation.
  • Standardizes the discovery property name so agent platforms can
    consume the identifier consistently across PSPs.

Motivation

The current "Secure Identifier" framing plus the access_token field
name create three concrete problems:

  1. Forces side-channel issuance. Merchants cannot publish the
    identifier through standard UCP discovery; tokenization handler
    hosts must run a separate provisioning channel even when there is
    no actual secrecy requirement.

  2. Misleads implementors on the threat model. Calling a
    non-secret routing value a "Secure Identifier" - and naming it
    access_token - implies it must be kept private, which leads to
    over-engineered storage and rotation flows that protect a value
    the handler host itself does not consider sensitive.

  3. Blocks self-service merchant onboarding. When the participant
    identifier can ride on discovery, an agent platform can read it
    directly from the merchant's profile and construct the
    binding.identity automatically. Today, every merchant must also
    be provisioned in a side channel.

The schema is named payment_identity.json and titled "Payment
Identity," with description "Identity of a participant for token
binding." The field name access_token does not fit that title. A
neutral name aligned with the schema's intent removes the confusion
at its source.

Goals

  • Clarify in the spec that the participant identifier carried in
    binding.identity may be either confidential or non-confidential,
    depending on the handler's threat model.
  • Allow non-confidential participant identifiers to be published in
    /.well-known/ucp payment_handlers[].config so agent platforms
    can consume them via standard discovery.
  • Standardize the discovery property name for the participant
    identifier under the non-confidential model.
  • Rename binding.identity.access_token to
    binding.identity.participant_id across the schema, OpenAPI,
    examples, and SDKs.
  • Update the payment_identity.json schema description so the prose
    no longer implies the value is always secret.

Non-Goals

  • Not removing the existing confidential-identifier model. Some
    handlers will continue to require it.
  • Not changing the wire shape of /tokenize or /detokenize (only
    one field within binding.identity is renamed).
  • Not specifying caller authentication. Handler-defined auth remains
    out of scope, consistent with the rest of the tokenization spec.
  • Not introducing a new credential type or new endpoint.

Detailed Design

Two security models for tokenization handlers

The processor-tokenizer example doc currently has a single
"Implementation Guide" section. Replace it with two named models
(applicable to all tokenization-style handlers, not just processor):

Model A - Confidential participant identifier

  • Handler host requires the identifier to be secret.
  • Issued out-of-band; not published in discovery.
  • Caller passes it as binding.identity.participant_id.
  • This is today's de facto behavior.

Model B - Public participant identifier with caller authentication

  • Handler host authenticates the caller (agent platform) via the
    request's Authorization header.
  • Participant identifier is non-secret and published in the merchant's
    UCP profile under payment_handlers[].config.
  • Caller reads the identifier from discovery and passes it as
    binding.identity.participant_id.
  • Suitable for closed partner networks where caller identity is the
    trust anchor.

Schema changes

source/schemas/shopping/types/payment_identity.json - introduce
participant_id, mark access_token as deprecated, and update the
top-level description so it does not assert secrecy:

{
  "title": "Payment Identity",
  "description": "Identity of a participant for token binding. Identifies the participant the token should be bound to. Handlers define whether this value is confidential or may be published in discovery; see the handler specification.",
  "type": "object",
  "properties": {
    "participant_id": {
      "type": "string",
      "description": "Identifier of the participant this token is bound to. May be confidential or non-confidential depending on the handler's threat model."
    },
    "access_token": {
      "type": "string",
      "deprecated": true,
      "description": "Deprecated. Use `participant_id`. Retained for backward compatibility; will be removed in a future spec version."
    }
  },
  "oneOf": [
    { "required": ["participant_id"] },
    { "required": ["access_token"] }
  ]
}

When both fields are present, participant_id takes precedence.

OpenAPI and example updates

source/handlers/tokenization/openapi.json - update every
binding.identity.access_token example body to use
binding.identity.participant_id (three card examples, two detokenize
examples). Examples should advertise the new field so implementors
copying from them adopt it.

docs/specification/examples/processor-tokenizer-payment-handler.md:

  • Drop "Merchant Secure Identifier" from line ~295.
  • Replace the single "Scenario A / Scenario B" block with the Model A
    / Model B description above.
  • Add a Model B example showing the participant identifier published
    in discovery.
  • Update all identity.access_token references to
    identity.participant_id.

docs/specification/examples/platform-tokenizer-payment-handler.md:

  • Update the prerequisites table row from identity.access_token to
    identity.participant_id.
  • Update the JSON example body that uses
    "access_token": "business_abc123".

docs/specification/examples/encrypted-credential-handler.md:

  • Update the prerequisites table row from identity.access_token to
    identity.participant_id.

docs/specification/tokenization-guide.md:

  • Update example bodies and the onboarding sentence
    ("Businesses receive access_token for handler identity") to use
    participant_id.
  • Note: line 159 (Authorization: Bearer {caller_access_token})
    refers to the caller's OAuth bearer credential, not the binding
    field. Leave unchanged.

docs/specification/payment-handler-template.md:

  • Update the three template tables that reference
    identity.access_token (lines ~100, ~194, ~334) to
    identity.participant_id.

docs/specification/payment-handler-guide.md:

  • Update the checklist line ("Identity maps to PaymentIdentity
    structure (access_token)") to reference participant_id.

docs/specification/identity-linking.md:

  • No change. The two access_token references here are OAuth bearer
    tokens, unrelated to binding.identity.

SDK updates (separate PRs in sibling repos)

The rename touches generated/handwritten model code in:

  • Universal-Commerce-Protocol/python-sdk -
    src/ucp_sdk/models/schemas/shopping/types/payment_identity.py
  • Universal-Commerce-Protocol/sdk -
    src/ucp_sdk/models/schemas/shopping/types/payment_identity.py

Companion PRs would update field names and any consumers. Conformance
tests that construct binding.identity (if any) would migrate at the
same time.

Discovery property convention (Model B)

When Model B applies, the participant identifier is published in
payment_handlers[].config. Recommend a standard property name so
agent platforms can read it consistently across handlers:

{
  "payment_handlers": {
    "com.example.processor": [{
      "id": "example_processor",
      "version": "{{ ucp_version }}",
      "config": {
        "participant_id": "merchant_xyz789"
      }
    }]
  }
}

The agent platform reads config.participant_id and constructs
binding.identity = { "participant_id": "merchant_xyz789" }.
Handlers operating under Model A continue not to publish this field;
the absence of it signals that the identifier must be obtained
out-of-band.

Risks and Mitigations

Security: The change does not weaken any existing handler.
Handlers requiring confidentiality continue to use Model A. Model B
is opt-in by the handler host. The mitigation against impersonation
under Model B is the caller's own authentication - exactly the same
property the rest of UCP relies on for authenticated platform calls.

Backward compatibility. Two paths, in order of preference:

  1. Additive (default). Introduce participant_id as a new field on
    payment_identity, accept either field on input with participant_id
    taking precedence when both are present, mark access_token as
    deprecated in the schema description, and remove it in a future
    spec version once adoption migrates. This is the safer path for
    anyone already wired to the current field name.
  2. In-place rename. Replace access_token with participant_id
    in one change. Cleaner end state and removes the misleading name
    immediately, but breaks any existing reader. Worth considering only
    if maintainers can confirm adoption of the current field name is
    negligible.

The schema and example changes in the Detailed Design section are
written for the additive path; the in-place rename is a one-line
delta on top (drop the access_token property entirely instead of
marking it deprecated).

Implementor confusion: Risk that implementors pick Model A or B
incorrectly. Mitigation: each handler specification states which
model applies, and discovery presence/absence of the participant
identifier serves as a runtime signal.

Test Plan

  • Schema validation: existing fixtures referencing
    binding.identity.access_token are updated to participant_id;
    validation continues to pass.
  • Conformance: discovery profiles publishing config.participant_id
    validate against the existing payment_handlers schema.
  • SDK round-trip: python-sdk and sdk model classes serialize and
    deserialize the renamed field correctly.
  • Documentation: every example doc round-trips through the rendered
    docs site without broken links or stale field names.

Graduation Criteria

Working Draft → Candidate:

  • Schema rename, OpenAPI updates, and example doc updates merged.
  • Companion SDK PRs merged (python-sdk, sdk).
  • At least one conforming handler specification (existing or new)
    names the model it implements.
  • TC majority vote to advance.

Candidate → Stable:

  • Adoption feedback from at least one Model A and one Model B
    handler.
  • TC majority vote to advance.

Implementation History

  • [YYYY-MM-DD]: Proposal submitted.

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions