Summary
Two related issues with how binding.identity is specified for
processor-tokenizer (and other tokenization) handlers:
-
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.
-
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:
-
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.
-
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.
-
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:
- 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.
- 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:
Candidate → Stable:
Implementation History
- [YYYY-MM-DD]: Proposal submitted.
Code of Conduct
Summary
Two related issues with how
binding.identityis specified forprocessor-tokenizer (and other tokenization) handlers:
The processor-tokenizer example doc describes the
binding.identity.access_tokenfield as a "Merchant SecureIdentifier" and the
payment_identity.jsonschema reinforces thiswith "Unique identifier for this participant, obtained during
onboarding with the tokenizer." Both framings push implementations
toward treating this value as a confidential bearer credential.
The field name itself -
access_token- reinforces the wrongmental model. Across OAuth, OpenID, and the rest of UCP (see
identity-linking.md, whereaccess_tokenis the OAuth bearercredential),
access_tokendenotes a secret that the holderpresents to authenticate. In
binding.identity, the value is theopposite: 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
Authorizationcredential.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/ucpdiscoveryprofile, where it is the natural place to publish.
This proposal:
vs public participant identifier).
binding.identity.access_tokentobinding.identity.participant_idto match the existing schemadescription ("Identity of a participant") and remove the
bearer-credential connotation.
consume the identifier consistently across PSPs.
Motivation
The current "Secure Identifier" framing plus the
access_tokenfieldname create three concrete problems:
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.
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 toover-engineered storage and rotation flows that protect a value
the handler host itself does not consider sensitive.
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.identityautomatically. Today, every merchant must alsobe provisioned in a side channel.
The schema is named
payment_identity.jsonand titled "PaymentIdentity," with description "Identity of a participant for token
binding." The field name
access_tokendoes not fit that title. Aneutral name aligned with the schema's intent removes the confusion
at its source.
Goals
binding.identitymay be either confidential or non-confidential,depending on the handler's threat model.
/.well-known/ucppayment_handlers[].configso agent platformscan consume them via standard discovery.
identifier under the non-confidential model.
binding.identity.access_tokentobinding.identity.participant_idacross the schema, OpenAPI,examples, and SDKs.
payment_identity.jsonschema description so the proseno longer implies the value is always secret.
Non-Goals
handlers will continue to require it.
/tokenizeor/detokenize(onlyone field within
binding.identityis renamed).out of scope, consistent with the rest of the tokenization spec.
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
binding.identity.participant_id.Model B - Public participant identifier with caller authentication
request's
Authorizationheader.UCP profile under
payment_handlers[].config.binding.identity.participant_id.trust anchor.
Schema changes
source/schemas/shopping/types/payment_identity.json- introduceparticipant_id, markaccess_tokenas deprecated, and update thetop-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_idtakes precedence.OpenAPI and example updates
source/handlers/tokenization/openapi.json- update everybinding.identity.access_tokenexample body to usebinding.identity.participant_id(three card examples, two detokenizeexamples). Examples should advertise the new field so implementors
copying from them adopt it.
docs/specification/examples/processor-tokenizer-payment-handler.md:/ Model B description above.
in discovery.
identity.access_tokenreferences toidentity.participant_id.docs/specification/examples/platform-tokenizer-payment-handler.md:identity.access_tokentoidentity.participant_id."access_token": "business_abc123".docs/specification/examples/encrypted-credential-handler.md:identity.access_tokentoidentity.participant_id.docs/specification/tokenization-guide.md:("Businesses receive
access_tokenfor handler identity") to useparticipant_id.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:identity.access_token(lines ~100, ~194, ~334) toidentity.participant_id.docs/specification/payment-handler-guide.md:PaymentIdentitystructure (
access_token)") to referenceparticipant_id.docs/specification/identity-linking.md:access_tokenreferences here are OAuth bearertokens, 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.pyUniversal-Commerce-Protocol/sdk-src/ucp_sdk/models/schemas/shopping/types/payment_identity.pyCompanion PRs would update field names and any consumers. Conformance
tests that construct
binding.identity(if any) would migrate at thesame time.
Discovery property convention (Model B)
When Model B applies, the participant identifier is published in
payment_handlers[].config. Recommend a standard property name soagent 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_idand constructsbinding.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:
participant_idas a new field onpayment_identity, accept either field on input withparticipant_idtaking precedence when both are present, mark
access_tokenasdeprecated 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.
access_tokenwithparticipant_idin 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_tokenproperty entirely instead ofmarking 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
binding.identity.access_tokenare updated toparticipant_id;validation continues to pass.
config.participant_idvalidate against the existing
payment_handlersschema.python-sdkandsdkmodel classes serialize anddeserialize the renamed field correctly.
docs site without broken links or stale field names.
Graduation Criteria
Working Draft → Candidate:
python-sdk,sdk).names the model it implements.
Candidate → Stable:
handler.
Implementation History
Code of Conduct