Skip to content

refactor!: make token binding vertical-agnostic - #746

Merged
prasad-stripe merged 5 commits into
mainfrom
prasad-stripe/polymorphic-binding
Aug 24, 2026
Merged

prasad-stripe merged 5 commits into
mainfrom
prasad-stripe/polymorphic-binding

Conversation

@prasad-stripe

@prasad-stripe prasad-stripe commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Description

Addresses the one exception #741 carves out: binding.json stays in source/schemas/shopping/types "as it explicitly references checkout sessions, which is not vertical agnostic."

That exception leaves the tokenization handler contract shopping-bound. The contract lives in source/handlers/, not under a vertical, and binding is a required field of both /tokenize and /detokenize. So after a refactor whose
stated purpose is vertical-agnostic payments, any non-shopping capability still has to fabricate a checkout_id to tokenize a credential. The same file already refs the credential from common/, so the asymmetry is only in binding.

What changes

  1. binding becomes a flat {type, id} pair in common/types/binding.json. type is the capability that owns the bound resource and refs reverse_domain_name.json, for example dev.ucp.shopping.checkout. id is the opaque identifier of that resource within the capability. One shape covers every capability, so there is no per-vertical binding schema and SDKs keep a single concrete model.
  2. Resource scope and participant scope become separate axes. 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 is unchanged in shape; only its position in the request moves. It is still referenced at shopping/types/payment_identity.json, since that is where it lives on main today; refactor!: Refactor Payment constructs (including related extensions) from shopping/ to common/ #741 relocates the file itself.
  3. Four normative rules now govern every tokenizer, stated in docs/specification/payment/tokenization.md: verification is exact equality over type and id, with unrecognized members ignored rather than rejected; binding.id is opaque and need not resolve; an unrecognized binding.type is not grounds for rejection; and every token is issued to exactly one participant, recorded at /tokenize and verified at /detokenize together with the caller's authority to act for it.

Why this is cheap for existing tokenizers

Binding was never semantically validated. A tokenizer cannot confirm that a checkout exists, so binding has always been an opaque equality token. This PR states that explicitly, which means an unrecognized type costs a tokenizer
nothing.

The one new risk is cross-type confusion, where a resource identifier from one capability collides with an identifier from another. The equality rule closes it in the same change: verification covers type and id together, so a matching id under a different type does not verify.

Credits

The flat {type, id} shape was proposed by @gsmith85 in review. Lifting identity out of binding was raginpirate's push. @igrigorik's #762, merged into this branch, separated the resource replay guard from participant authorization and is what the equality and participant rules now say.

Category (Required)

  • 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

Related to Phase 2 in #520. Includes #762 by @igrigorik.

Not stacked. This branch is based directly on main and merges independently of #741. The two PRs overlap in two places, and the overlap makes #741 smaller rather than larger: this PR deletes source/schemas/shopping/types/binding.json, which is the one file #741 carves out as an exception, and adds two
payment_identity $refs in source/handlers/tokenization/openapi.json, a file #741 already edits. Whichever lands first, the other rebases. Happy to sequence with @jingyli either way.

Requesting this rides #741's release rather than a later one. Both are breaking changes to the same schema surface, so landing them together means implementers migrate binding once.

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.

On the unchecked boxes: this change has no executable test surface, the JSON examples in the touched docs are validated by scripts/validate_examples.py in CI, and generate_models.sh and python_sdk do not exist in this repository.

Breaking change

Three changes to the same surface:

  1. https://ucp.dev/schemas/shopping/types/binding.json moves to https://ucp.dev/schemas/common/types/binding.json.
  2. checkout_id is replaced by type plus id. A checkout binding becomes {"type": "dev.ucp.shopping.checkout", "id": "<checkout_id>"}.
  3. identity moves out of binding to a sibling field on the /tokenize and /detokenize request bodies.

Migration for a shopping implementer is one renamed field, one added field, and moving identity up one level. Because releases are separate snapshots, previously published versions keep their paths.

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

There is a validation gotcha in the proposed shape: /tokenize and /detokenize reference only common/types/binding.json, which requires type but does not require a resource identifier. This therefore validates:

{
  "type": "dev.ucp.shopping.checkout"
}

checkout_binding.json requires checkout_id, but nothing machine-readable connects the discriminator to that schema. The effective handler contract becomes less strict while adding a required field to every client. Doh.

That leads to a design question: do we need a separate type discriminator?

If you squint, the resource identifier is already self-describing:

{ "checkout_id": "checkout_123" }        <- existing
{ "booking_id": "booking_123" }            <- new
{ "reservation_id": "reservation_123" }  <- new

If food re-uses checkout, then it can adopt checkout_id, whereas a token bound to an actual booking would use booking_id...type adds value only if two semantically different resources use the same identifier field, and it seems like we can avoid it.

cc @raginpirate

@jingyli jingyli added the TC review Ready for TC review label Aug 20, 2026
@gsmith85 gsmith85 added this to the 2026-08-24 milestone Aug 21, 2026
@gsmith85
gsmith85 self-requested a review August 21, 2026 00:22

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

Looking at the constraints on both sides:

  1. Tokenization handlers are horizontal specifications and should not hardcode Shopping's checkout_id.
  2. The current PR shape creates a validation gap at the OpenAPI layer: because common/types/binding.json requires only type, a request omitting a resource identifier will pass schema validation.
  3. Defining separate binding schemas per vertical (checkout_binding.json, booking_binding.json, etc.) adds schema maintenance overhead and requires polymorphic model handling in SDKs.

Proposed Alternative: Single binding { type, id }

We can address both requirements with a single common/types/binding.json that requires type and id:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://ucp.dev/schemas/common/types/binding.json",
  "title": "Binding",
  "description": "Binds a credential or token to a specific capability resource and participant identity.",
  "type": "object",
  "required": ["type", "id"],
  "properties": {
    "type": {
      "$ref": "reverse_domain_name.json",
      "description": "The capability type that owns the bound resource (e.g., dev.ucp.shopping.checkout, dev.ucp.dining.reservation)."
    },
    "id": {
      "type": "string",
      "description": "The resource identifier within the capability domain (e.g., checkout ID, reservation ID, booking ID)."
    },
    "identity": {
      "$ref": "payment_identity.json",
      "description": "Optional participant identity to bind to."
    }
  },
  "additionalProperties": false
}

On the Wire:

  • Shopping: {"type": "dev.ucp.shopping.checkout", "id": "chk_12345"}
  • Lodging: {"type": "dev.ucp.lodging.booking", "id": "bk_98765"}
  • Dining: {"type": "dev.ucp.dining.reservation", "id": "res_54321"}

Trade-offs:

  • Validation: OpenAPI validators can strictly enforce that both type and id are present on /tokenize and /detokenize.
  • Schema Maintenance: Eliminates checkout_binding.json and the need to create new binding schemas for future verticals.
  • SDK Generation: Produces a single concrete Binding model across SDK languages rather than an inheritance hierarchy or union type.
  • Tokenizer Logic: Tokenizers verify exact equality over type, id, and identity.
  • Migration: Requires existing shopping clients to supply type and rename checkout_id to id. Given that #741 and #746 are already breaking refactors (refactor!:) ahead of the release snapshot, this aligns all verticals on a uniform contract.

@igrigorik , @prasad-stripe What are your thoughts on this approach?

@prasad-stripe

Copy link
Copy Markdown
Contributor Author

Thanks @igrigorik and @gsmith85, both points land.

@igrigorik: the validation gap is real. Referencing only the base from /tokenize and /detokenize means {"type": "dev.ucp.shopping.checkout"} validates with no resource identifier, which is a looser contract than requiring checkout_id.

@gsmith85: adopting your shape. It closes that gap with a schema constraint instead of a convention, drops checkout_binding.json, and gives SDKs one concrete Binding model.

Keeping type

  1. type is governed, field names are not. It refs reverse_domain_name.json and names a capability, so its values are enumerable against the capability registry. Identifier field names have no registry, and allowing future verticals means additionalProperties stays open, so {"chekout_id": "..."} validates clean.
  2. "If food re-uses checkout, then it can adopt checkout_id" is the collision case: two different resources through one field, different binding scope for the tokenizer. That condition is met by the first vertical that reuses checkout.
  3. Exact-equality verification needs a closed shape. Binding is a replay guard verified by equality over the whole object. Against an open bag, "whole object" is ambiguous. Against {type, id, identity} closed, it is exact.

Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://ucp.dev/schemas/common/types/binding.json",
  "title": "Binding",
  "description": "Binds a credential or token to a specific capability resource and participant identity. Prevents reuse across different resources or participants.",
  "type": "object",
  "required": ["type", "id"],
  "properties": {
    "type": {
      "$ref": "reverse_domain_name.json",
      "description": "The capability that owns the bound resource, for example dev.ucp.shopping.checkout. MUST be a capability name declared in the UCP namespace."
    },
    "id": {
      "type": "string",
      "description": "Opaque identifier of the bound resource within the owning capability, for example a checkout identifier."
    },
    "identity": {
      "$ref": "payment_identity.json",
      "description": "The participant this token is bound to. Required when acting on behalf of another participant, for example an agent tokenizing for a business. Omit when the authenticated caller is the binding target."
    }
  },
  "additionalProperties": false
}

Three rules to land with it

The schema constrains the wire. These constrain tokenizer behavior, and without them the generalization is not safe for deployed implementations.

  1. Verification. A Tokenizer MUST verify a binding by exact equality over the complete object, including type, id, and identity. Partial matches MUST NOT be accepted.
  2. Opacity. A Tokenizer MUST treat binding.id as opaque, and MUST NOT parse it or verify the resource exists. Already true in practice, since a tokenizer cannot confirm a checkout exists. Stating it is what makes one generic id safe across verticals.
  3. Forward compatibility. A Tokenizer MUST NOT reject a request solely because it does not recognize binding.type. Without this, every shopping-only tokenizer becomes a blocker for the first new vertical. With it, and given rule 2, generalizing costs deployed tokenizers nothing.

Rule 3 is the one I want your read on. It is the difference between a schema that permits other verticals and a protocol where they can reuse deployed tokenizers.

On additionalProperties: false

Support closing it, with one flag: today's binding.json has no additionalProperties key, so this tightens beyond the current contract, not just beyond this PR. Correct for a replay guard, but if anyone relies on extra keys in binding, now is the time to say so.

Migration

I own Stripe's UCP tokenization handler, so concretely: one field becomes two at the /tokenize and /detokenize boundary plus the published $refs in our handler declaration. Contained, and cheaper before the release snapshot than after, which is @gsmith85's point and I agree with it.

Next

Respinning to common/types/binding.json added, shopping/types/binding.json removed, checkout_binding.json dropped, handler plus its five inline examples updated, three rules added to the tokenization spec. 43 files to 5 once #741 lands.

@jingyli @raginpirate: given the dev.ucp.common versus dev.ucp.common.payments thread on #741, where do payment types finally land under common/? binding.json sits beside payment_identity.json and reverse_domain_name.json as same-directory refs, so I would rather write the path once.

cc @raginpirate

@prasad-stripe
prasad-stripe force-pushed the prasad-stripe/polymorphic-binding branch from 3736145 to 1edae42 Compare August 21, 2026 16:11
@prasad-stripe
prasad-stripe marked this pull request as ready for review August 21, 2026 16:58

@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 taking a stab at cleaning this up. I am challenging the scope of the primitives use case since we're thinking about extending it elsewhere, but another part of me thinks it might be best to actually delete this object and just inline the rev-dns approach inside of the openrpc example, as I'm not sure how we'll use this across UCP beyond the example files (YAGNI).

I'll also push back strongly on closing the object; no need IMO, extending this object seems valid.

Comment on lines +13 to +16
"id": {
"type": "string",
"description": "Opaque identifier of the bound resource within the owning capability, for example a checkout identifier."
},

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.

Truthfully, a binding associated to a credential may be actually scoped to N unique resources and not just one. The example tokenizer was very specific to checkout and as we look beyond it this shape doesn't extend very well.
I'd lift identity out of this and either make it simply an open rev-dns bag with properties being string id values in the tokenizer openrpc, or make it an array of type + id without identity in it.

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.

Taking the identity point: agreed. identity moves out to a sibling field on /tokenize and /detokenize. Resource scope and participant scope are different axes, and nesting identity inside a plural binding would be the wrong shape.

On N resources, I would rather not make binding plural here. When a token needs to cover several resources, bind it to the resource that contains them: {"type": "dev.ucp.travel.itinerary", "id": "itn_1"}, or a cart, or an order group. The container is the resource. Plural also forces decisions this PR should not make, such as whether /detokenize presents every binding or a subset, and whether one resource can be released without the others.

On the open reverse-DNS bag: it is plural natively, but it moves the governed value into a property name, so nothing requires an entry and a misspelled capability validates clean. That is the gap @igrigorik flagged. Keeping the capability in a value lets reverse_domain_name.json constrain it.

Worth saying that {type, id} is what makes plural possible later, and additively, since each element would already be a complete binding. {checkout_id} could not. So this shape is a prerequisite for what you are describing rather than an obstacle to it.

Given the container pattern covers the case, I would leave plural until something actually requires it rather than commit to it now.

@prasad-stripe

Copy link
Copy Markdown
Contributor Author

Thanks @raginpirate, two answers.

additionalProperties: false: dropping it. Agreed, extending this object is reasonable.

Keeping binding.json as its own file, though. You're right on the usage: it's referenced from exactly two places in source/, both in handlers/tokenization/openapi.json. Two things break if it's inlined:

  1. reference.md generates the published Schema Reference from common/types/, so Binding drops out of the docs entirely.
  2. The $id https://ucp.dev/schemas/common/types/binding.json is a URL handler declarations point at. Stripe's handler publishes $refs to ucp.dev schema URLs, and I'd expect others to do the same.

There's also a local consistency cost: the same request body already $refs payment_credential.json, so refing one and inlining the other leaves that body half $ref and half literal.

My read is that binding is single-use because tokenization is currently the only horizontal handler contract, not because the shape is narrow.

@prasad-stripe
prasad-stripe force-pushed the prasad-stripe/polymorphic-binding branch 2 times, most recently from 1be2813 to 3ada90e Compare August 21, 2026 21:28
igrigorik added a commit that referenced this pull request Aug 21, 2026
   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`.

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

@prasad-stripe @raginpirate the shape y'all converged on makes sense, but I'm tripping up over what reads as contradictory rules. PTAL @ #762 - does this make sense, or am I holding it wrong?

@prasad-stripe prasad-stripe changed the title refactor!: make token binding polymorphic across verticals refactor!: make token binding vertical-agnostic Aug 24, 2026
prasad-stripe and others added 3 commits August 24, 2026 12:51
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.
   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`.
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.
@prasad-stripe
prasad-stripe force-pushed the prasad-stripe/polymorphic-binding branch from 878abae to b2a0412 Compare August 24, 2026 16:58
@prasad-stripe

Copy link
Copy Markdown
Contributor Author

@prasad-stripe @raginpirate the shape y'all converged on makes sense, but I'm tripping up over what reads as contradictory rules. PTAL @ #762 - does this make sense, or am I holding it wrong?

@igrigorik #762 is merged in. You were right that rule 1 couldn't compare identity once it left binding: a business omits it, its PSP includes it, so equality passes at most one legal shape.

Two tweaks to your rules:

Rule 1. Dropped "members other than type and id are therefore not covered by the replay guard" as too permanent. Now: MUST NOT reject unrecognized members, MUST ignore them when comparing, MAY include members defined by an extension it implements. Unknown stays out; known-by-extension can opt in.

Rule 4. Added one sentence: delegation is handler-defined and out of scope. Otherwise rule 4 reads as requiring a mechanism UCP doesn't define.

Rule 2, the /detokenize prose fix, and minLength: 1 are yours unchanged. PTAL.

This PR now stands on its own. It's based directly on main, merges cleanly, and no longer depends on #741.
CI green. @jingyli, we should sequence the two merges, either order works and I'll rebase behind you if that's easier.

Comment thread docs/specification/payment/tokenization.md Outdated
Co-authored-by: Ilya Grigorik <ilya@grigorik.com>

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

took a few iterations but glad we took the long route!

lgtm 👍

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

👍 lgtm thanks for the better abstraction on binding!

@amithanda

Copy link
Copy Markdown
Contributor

This PR does a great job updating platform-tokenizer-payment-handler.md, processor-tokenizer-payment-handler.md, guide.md, and tokenization.md to reflect the generic {type, id} binding shape and separated identity.
However, encrypted-credential-payment-handler.md (the third payment handler example) still explicitly refers to checkout_id in its binding verification step and security requirements:

  • Step 3 (Line 217): "Confirm the decrypted checkout_id matches the current checkout"
  • Security Table (Line 352): "| Binding embedded | checkout_id MUST be included in encrypted payload to prevent replay |"

Could we update these two occurrences to reference the generic binding object (type and id) as well? This would ensure consistent terminology across all three payment handler examples in the specification.

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.
@prasad-stripe
prasad-stripe merged commit 014cbe8 into main Aug 24, 2026
18 checks passed
@prasad-stripe
prasad-stripe deleted the prasad-stripe/polymorphic-binding branch August 24, 2026 21:54
@prasad-stripe

Copy link
Copy Markdown
Contributor Author

This PR does a great job updating platform-tokenizer-payment-handler.md, processor-tokenizer-payment-handler.md, guide.md, and tokenization.md to reflect the generic {type, id} binding shape and separated identity. However, encrypted-credential-payment-handler.md (the third payment handler example) still explicitly refers to checkout_id in its binding verification step and security requirements:

  • Step 3 (Line 217): "Confirm the decrypted checkout_id matches the current checkout"
  • Security Table (Line 352): "| Binding embedded | checkout_id MUST be included in encrypted payload to prevent replay |"

Could we update these two occurrences to reference the generic binding object (type and id) as well? This would ensure consistent terminology across all three payment handler examples in the specification.

Thanks @amithanda, good catch. Fixed both in e32a1e8e.

Step 3 now verifies the decrypted binding by comparing type and id, and the Binding embedded row says the binding object goes in the encrypted payload. Also swept the rest of the repo: nothing else left.

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 TC review Ready for TC review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants