Skip to content

feat: Add Payment Authentication Actions - #458

Open
raginpirate wants to merge 2 commits into
mainfrom
raginpirate/embedded-frame-action
Open

feat: Add Payment Authentication Actions#458
raginpirate wants to merge 2 commits into
mainfrom
raginpirate/embedded-frame-action

Conversation

@raginpirate

@raginpirate raginpirate commented May 20, 2026

Copy link
Copy Markdown
Member

Description

Introduces dev.ucp.shopping.payment_authentication, a Checkout extension defining standard browser-surface Actions for payment authentication:

  • dev.ucp.payment.device_data_collection
  • dev.ucp.payment.three_ds_challenge

This PR is stacked on #582, which defines the common extension-owned Actions primitive. Payment Authentication contributes the concrete Action types, config, rendering behavior, security requirements, notification protocol, deadlines, and Checkout integration needed for DDC and 3DS challenges.

The approach

EMVCo defines the interaction between the issuer’s ACS and the merchant’s 3DS provider, mediated by the cardholder’s device and card-network Directory Server. Businesses and platforms do not have to be involved directly in this specification; often Payment Providers create an abstraction layer around the concerns of the EMVco spec. With this world view in mind, UCP attempts to standardize the abstraction layer that businesses and payment providers truly need to offer when handling 3DS and DDC on arbitrary business OR platform surfaces; a simple signal pattern to resolve or reject the action.

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

#517
#582

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.

@aneeshali aneeshali left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks a lot @raginpirate for this PR. Added a few comments based on the last discussion.

Comment thread docs/specification/checkout.md Outdated
Comment thread docs/specification/checkout.md Outdated
Comment thread docs/specification/checkout.md Outdated
@raginpirate
raginpirate force-pushed the raginpirate/embedded-frame-action branch from fa54c5c to 021b31b Compare June 11, 2026 13:28

@aneeshali aneeshali left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks a lot @raginpirate for a productive discussion. Left a couple minor comments.

Comment thread docs/specification/payment-actions/device-data-collection.md Outdated
Comment thread docs/specification/checkout.md Outdated
@raginpirate raginpirate changed the title introduce some kind of action primitive; still a wip 3ds actions spec; WIP Jun 26, 2026
Comment thread docs/specification/payment-actions/device-data-collection.md Outdated
Comment thread source/services/payment-actions/embedded.openrpc.json Outdated
Comment thread docs/specification/payment-actions/device-data-collection.md
```json
{
"jsonrpc": "2.0",
"method": "pa.done",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

rather than having a separate postMessage design for UCP and our own direct integration or clientside SDK use we would likely add key:values in the postMessage for our own purposes. I would hope/assume these would be ignored by UCP?

these could be additional identifiers for support purposes or expanding on the detail of the outcome e.g. authenticated, rejected, unavailable....

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yup totally; additional properties is the supported default for any object passed over UCP. This can empower you to adhere to the subset standard while growing the superset and I'd advocate you can always push for more of that superset when it makes sense for that information to live with the platform and not just the business!

@kmfreeman kmfreeman Jul 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@raginpirate sorry if this is a bit late but a few additional thoughts came up now I'm getting in to the details of feature creation:

  • should method actually be result as this is the one direction postMessage?
  • if you make result (or method if I'm wrong) an object the design becomes more open in terms of allowing others to use the standard jsonrpc result design e.g.
{
  "jsonrpc": "2.0",
  "result": {
    "event": "pa.done",  // used by UCP
    "outcome": "3dsAuthenticated",  // could be any key:value for use by the gateway e.g. used by clientside SDK
  },
  "id": 1
}

This is all within the context of wanting to have a single postMessage design for both direct 3ds integration (with the gateway) and extending it for UCP usage

Also means that for other postMessages we plan to omit such as when a challenge is required we can just remove the event from the result object as UCP does not need to react to this
we would probably add pa.challenged or pa.prompt instead so can be parsed reliably for the challenge case

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks @kmfreeman — using one postMessage design for both direct gateway integrations and UCP is definitely the goal!

I believe method is the correct JSON-RPC shape here. This is because we're expecting the frame to postMessage that event to the parent page, and result is undefined because there is no expected response to carry back to the parent page after receiving it.

Additional params such as outcome or provider correlation data can still support a direct gateway SDK. UCP Platforms only process the fields defined by the active Action contract and ignore other params.

@raginpirate
raginpirate force-pushed the raginpirate/embedded-frame-action branch from 021b31b to 8f3d43b Compare July 6, 2026 06:59
@raginpirate
raginpirate changed the base branch from main to raginpirate/actions July 6, 2026 17:57

@aneeshali aneeshali left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks Daniel for driving this alignment across may parties.

Comment thread docs/specification/payment-actions/device-data-collection.md Outdated
Comment thread source/services/payment-actions/embedded.openrpc.json
@raginpirate raginpirate changed the title 3ds actions spec; WIP feat: Add Payment Authentication Actions Jul 17, 2026
@raginpirate
raginpirate force-pushed the raginpirate/embedded-frame-action branch from 8f3d43b to 33116ac Compare July 17, 2026 14:26
@raginpirate
raginpirate changed the base branch from raginpirate/actions to feat/actions July 17, 2026 14:26
@raginpirate
raginpirate marked this pull request as ready for review July 17, 2026 14:27
@raginpirate
raginpirate requested a review from aneeshali July 17, 2026 14:27
@raginpirate raginpirate self-assigned this Jul 17, 2026

@aneeshali aneeshali left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Really appreciate the work on this and the related discussions, @raginpirate. Left a couple of comments to fully understand the implementation specifics.

and retrieve Checkout. It does not mean authentication or payment succeeded.
After
closing the surface, the Platform uses Get Checkout and does not call Complete
Checkout again to resume the accepted operation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With the approach to complete the flow by calling Get Checkout rather than Complete Checkout, please confirm if the expectation is that the Platform can only perform 2 things:

  • Resume the flow if further actions are needed, like proceed with challenge once DDC is complete.
  • Mark the flow as completed if the Get Checkout returns a response similar to a successful non-3DS complete checkout call.

I'm wondering how will some of the failure cases be handled gracefully in this scenario. For example, if 3DS fails for whatever reason, the platform will make a subsequent Get checkout call, and the merchant may repeatedly return the same action payload as before. If the platform wants to let the user choose a different payment method or similar remedies, there may not be a way to do it in this scenario. Please share your thoughts on how such cases can be handled.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

If the Business learns a terminal provider outcome, it must remove the original Action, replace it with new work under a new ID, or advance the Checkout. It must not keep returning an occurrence that can no longer be processed, and the Platform must not reopen the same occurrence.

An incomplete Checkout can still be updated to select another instrument. Once Complete Checkout has been accepted and the Checkout is complete_in_progress, Update and another Complete are intentionally unavailable. Recovery at that point follows the handler fallback, buyer handoff through continue_url, or cancellation and creation of a new checkout.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I am not sure a GET call is the correct approach here. A Platform calling GET may well trigger a mutating action on the checkout resource on the Business side. This should be modelled as a PUT or PATCH on the checkout resource.

It would also means the agent cannot pass on any extra data that might be required as an output of the action which took place. To be extensible, I think an action outcome executed on the Platform should allow additional properties to be passed to the Business. An action definition should define which properties are required/allowed and the checkout update call (via a PATCH/PUT/POST) can submit these as part of the action outcome.

Taking the example from PR #582 , the response to the complete checkout call contains the required actions. As this forms part of the checkout contract and follows REST principles, it would naturally follow a subsequent call to update the checkout resource could also update the action with any extra fields required:

Response:

"actions": {
    "com.example.identity.student_verification": [
      {
        "id": "verify-student-1",
        "config": {
          "verification_url": "https://business.example.com/verify/abc"
          "required_fields" : ["student_id"]
        }
      }
    ]
  },

Request:

"actions": {
    "com.example.identity.student_verification": [
      {
        "id": "verify-student-1",
        "student_id": "123"
      }
    ]
  },

@simonwfarrow simonwfarrow Jul 29, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

To add a more specific 3ds example, if an external MPI is in use , allowing the outcome of the 3ds action to carry an identifier that identifies the authentication result on the 3ds provider would be very useful.

i.e.

"actions": {
    "dev.ucp.payment.three_ds_challenge": [
      {
        "id": "three_ds_challenge",
        "authentication_id": "athlf5Y0SJfc_azJ746ry8Ja"
      }
    ]
  },

Comment on lines +185 to +187
- If the surface remains invisible and never requests Buyer interaction, the
Business emits device data collection.
- If the surface may display a Buyer challenge, the Business emits 3DS challenge.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Some PSPs are adopting a slightly different approach for their merchants in the standard 3DS flows. They provide a single URL, which performs device data collection in a visible manner with some standardized loading screen. Depending on the result of DDC, they may either redirect the user to a challenge UI or just complete the flow and pass the control back to the merchant. We could either consider this as a subcase of three_ds_challenge or we could call out as a separate option. I think what the loading screen will look like is out of scope of this PR but providing clarity on that scenario will be helpful considering this is a relevant pattern.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

One wrapper URL may orchestrate both provider steps. If the surface may ever become buyer-visible, the Business emits a three_ds_challenge Action, because honestly thats what they are doing as the action (ddc is simply a side-effect of their modal's purpose). A device_data_collection Action must remain invisible throughout its lifetime and should only be used by providers that do separate the steps.

Base automatically changed from feat/actions to main July 23, 2026 03:54
@raginpirate
raginpirate force-pushed the raginpirate/embedded-frame-action branch from 33116ac to 2bfda8e Compare July 24, 2026 16:50
@raginpirate

raginpirate commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

👋 I’ve force-pushed now that the base Actions primitive has landed.

The update aligns with the landed Action and Checkout lifecycle contracts:

  • Removed the old occurrence-level required concept; each Action type statically defines what it gates.

Additional improvements I pushed in here:

  • Signal the payment instrument id so its clear which element we're acting on.
  • Added an action.ready handshake for both surfaces. The frame identifies the Action and extension version, and waits for acknowledgment before beginning work. better matching how all embedded protocols work in the spec today.
  • Clarified origin validation, rendering isolation, reconciliation, fallback, and handler-authoring requirements.

"id": "ddc-1",
"config": {
"payment_instrument_id": "instrument_1",
"url": "https://payments.example.com/ucp/payment/ddc/session_123"

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.

How do we plan to support App channel which requires sdk and no url? Is that deferred to future? Even if we plan to defer it, do we have high level idea on how that will come in? Will this same extension expand to support it or will it be a sibling action e.g dev.ucp.payment.three_ds_sdk_challenge or similar?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes we should defer, and I believe it should be independent from this extension entirely; not just a different action.

There is no one unified 3DS SDK at this time: EMVCo specifies the SDK's interfaces and approves implementations, but ships none — so every provider carries their own approved build. As a result what we'd be looking at is a unique SDK per provider that must be negotiated as an independent extension today, enabling:

  • a unique rev-dns signalled contract for what SDK the platform has agreed to implement and the business has agreed to advertise.
  • the business to give the native signal to their payment provider at payment time, and propagate the native signals back to the platform.
  • the platform to then embed that specific provider's contract for how to use their specific SDK, and a result to finally land with the business (either in-spec or out-of-band).

Even if we imagine one SDK existing and shared across all providers, I still think this fits the bill for a fundamentally isolated extension because theres almost nothing shared between the two specs: different trust, implementation, parameters, negotiation.


The Business **SHOULD** provide a Business- or provider-operated wrapper URL that
owns any provider-specific POST, such as submitting CReq to an ACS challenge
URL. The Platform performs an ordinary navigation to `config.url`; it

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.

How are we thinking about the trust anchoring (invariant 11 from #517)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good prompt to re-discuss 11. The handler specification names the origins its Action surfaces load from — the handler guide and publishing checklist now ask for them — and a Platform declines any occurrence whose config.url origin isn't among them, so the expectation exists before the Action is emitted and a response can't bootstrap its own trust. This differs a bit from 11, which overcorrects to a relation between providers and platforms; payment handlers can (and often should) state that businesses are allowed to advertise 3ds urls themselves, with any given business using only the subset it actually operates. Platforms have the easy escape hatch of escalation if they do not actively support the full subset as well.

}
},
{
"name": "error",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In device-data-collection.md and three-ds-challenge.md, codes like action_unavailable, action_expired, and action_failed are listed as "recommended" surface-level error codes for action.error. To ensure consistency and prevent payment providers from inventing custom error strings, can we formalize these error codes as an enum here?

@raginpirate raginpirate Jul 29, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good question, I've always deeply struggled with enums in UCP (or I should say, JSON schemas) to be honest 😅

But... code is an open string everywhere in UCP — error_code.json says freeform codes are permitted — so an enum here would make this extension stricter than the core type it references, and closing an enum makes it so we have to author a breaking change if we try to extend it. It also wouldn't change any behaviour: a Platform unmounts and reconciles the same way for all three codes and for any code it doesn't recognize.

Comment thread docs/specification/payment-authentication.md Outdated
Comment thread docs/specification/payment-authentication.md Outdated
interaction with the rest of Checkout, and **SHOULD** show a loading state until
its content renders. Web Platforms use an iframe or other handler-approved
browser context. For messages from a native surface to the Platform, native
Platforms **MUST** expose a JSON-string bridge through

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since Android bindings typically expect a method signature taking stringified JSON, whereas iOS WebKit expects a JavaScript object, should we add explicit JS snippet examples for both Android and iOS bridge invocations in the spec to avoid SDK interoperability issues? This will provide clarity for developers building cross-platform bridge requirements

@raginpirate raginpirate Jul 29, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The bridge is already a JSON string both ways: the Embedded Protocol defines postMessage(message: string) carrying a JSON-RPC message, and this spec says the same for the ready response back to the surface. WKWebView accepts a string fine, so one surface implementation works on both without branching.

Examples are not a bad idea, that could be a good addition to bring to embedded-protocol.md so every capability gets them rather than just this one! But maybe we can descope that from this specific PR.

Comment thread docs/specification/payment-authentication.md
@raginpirate

Copy link
Copy Markdown
Member Author

Pushed up a small doc improvement based on last comments, please re-review!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants