feat: add 3DS2 support to spec and schemas#421
Conversation
2825f10 to
e6c573c
Compare
e6c573c to
0f012f2
Compare
0f012f2 to
a0a7d0a
Compare
prasad-stripe
left a comment
There was a problem hiding this comment.
Thanks @aneeshali for putting this together! The vendor-agnostic approach with the merchant as orchestrator and host as execution environment is a clean design, and reusing complete_in_progress with the action/result loop keeps the spec surface minimal. Directionally this is strong.
A few architectural suggestions and minor questions/comments inline:
Capability negotiation for challenge support
This PR introduces challenge actions but doesn't participate in UCP's existing delegation negotiation. The host currently has no way to signal whether it can render 3DS challenges (iframes, popups). Should there be a payment.challenge delegation identifier? If the host doesn't declare it, the merchant knows upfront and can either attempt frictionless-only, offer payment instruments that don't require 3DS, or fall back to requires_escalation + continue_url. Without this signal, the merchant may send a challenge action that the host can't execute. Can we note in the guide that if the host doesn't declare payment.challenge, the merchant should fall back to requires_escalation + continue_url?
Frictionless path documentation
The flow implicitly supports frictionless (merchant returns completed directly if no challenge is needed), but the guide only documents the challenge path. Can we add an example showing the frictionless case (e.g., DDC completes, merchant returns completed without Phase 3)? This will help implementors understand the challenge phase is conditional.
Instrument switching during challenge flow
If the buyer starts 3DS for card A, gets a challenge, and decides they'd rather use card B or a wallet, what's the path? Can we send /complete with a different instrument while the checkout is in complete_in_progress for card A? Or does the session need to be abandoned? Being able to switch without restarting the session would help conversion.
| "challenge" | ||
| ] | ||
| }, | ||
| "status": { |
There was a problem hiding this comment.
status currently has "success" and "failure" as examples. Different failure modes have different implications for the host: buyer abandoned the challenge (retry same instrument), ACS timed out (transient, retryable), card not enrolled (guide buyer to a different payment instrument), technical error (terminate). UCP checkout already has a severity model on messages (recoverable, requires_buyer_input, unrecoverable) that prescribes recovery actions. The same values and pattern could be applied to challenge results so the host knows how to recover. The current set may need an additional value like try_different_instrument for cases where the issue is specific to the selected payment instrument (e.g., card not enrolled).
There was a problem hiding this comment.
I have changed them to be callback_received and abandoned making them more specific. Also expanded the error handling to cover what exactly is expected in these scenarios and the corresponding interactions.
| "type": { | ||
| "type": "string", | ||
| "description": "The mechanism for the action. Well-known values: `hidden_iframe`, `popup`.", | ||
| "examples": [ |
There was a problem hiding this comment.
type currently has "hidden_iframe" and "popup" as examples. There are authentication flows where no browser interaction is needed (e.g., out-of-band approval via a banking app). How would the host handle an action that doesn't require rendering anything? Consider supporting a type that signals the host to show a waiting state rather than render an iframe or popup.
For such flows, the completion signal becomes a question:
- Short-term: Host shows a "I've approved, continue" button. Buyer taps it after approving on their phone. Host calls /complete. In the happy path, PSP has already received the approval and merchant returns completed. Race condition: buyer approved but PSP confirmation hasn't arrived yet, so merchant returns complete_in_progress again. The simple solution is to retry, but since there is no new information in the request, the merchant may not be able to distinguish a retry from a duplicate. To mitigate this, consider adding a retry_attempt field so the merchant knows this is an intentional poll, not an accidental replay. Merchant specifies max_retries and retry_after_seconds in the action to bound the polling. Note: this retry/idempotency concern is specific to the out-of-band flow; the standard iframe/popup flows always carry new data (DDC result, cres) in each /complete call.
- Long-term: The host already provides callback_url. For out-of-band flows, the merchant (or PSP) could POST to this URL server-to-server when authentication resolves, giving the host a push notification to resume without buyer interaction. This eliminates the race condition but requires clarifying that callback_url is a server-reachable endpoint (not only an iframe navigation target). This may warrant a separate mechanism or a dedicated notification_url to avoid overloading callback_url semantics.
Note: The out-of-band flow could be a fast follow-up PR rather than complicating this one, but the type field should be designed to accommodate it without breaking changes.
There was a problem hiding this comment.
Thanks, I added a section covering some high level details under section Out-of-Band Authentication. Also added a TODO for expanding on this later.
For the short term, please share your thoughts on extending the hidden iframe approach path to support this case, where the effort will be minimal for the business/PSP. If you see concerns with that approach, we can stick to the continue button approach. Alternatively, we can provide both options and let the Platform+Business choose the right path. Please share your thoughts.
There was a problem hiding this comment.
Thanks for adding the OOB section. For the short term, the hidden iframe approach is pragmatic and gets us to a working state for web contexts without much lift on the business/PSP side.
That said, I think the proper long-term solution here is server-to-server callbacks (webhooks). OOB auth completion is inherently a server-side event (the ACS notifies the PSP via RReq), and relying on client-side polling or long-lived iframes to surface that signal is fragile, especially on mobile or in restricted webview contexts. I'd suggest we keep the TODO and note that the server callback path is the intended durable solution, with the hidden execution approach as an acceptable interim for web-only platforms.
No need to block the PR on this; just want to make sure we're aligned on direction.
| "format": "uri", | ||
| "description": "The URL where the result should be sent or where the interaction completes." | ||
| }, | ||
| "action": { |
There was a problem hiding this comment.
The action object has no expiry. How long should the host wait for a DDC iframe or challenge popup to complete before giving up? Without guidance, hosts will implement inconsistently. Should there be an expires_at or timeout_seconds field on the action so the merchant can signal how long the host should wait before reporting failure?
There was a problem hiding this comment.
Added a new field expires_at to address this.
There was a problem hiding this comment.
An absolute timestamp requires clock synchronization between business and platform, which is unreliable. For the typical 5-10s DDC timeout, an interval (e.g., timeout_seconds: 10) is more practical. It tells the platform how long to wait after posting to the 3DS Method URL, without needing synchronized clocks. WDYT?
| "token": "examplePaymentMethodToken" | ||
| }, | ||
| "challenge": { | ||
| "callback_url": "https://p.g.com/challenge/callback/session_123" |
There was a problem hiding this comment.
The host provides callback_url here, and the sequence diagram shows "DDC Result via Callback" and "Challenge Result via Callback." But the host also calls /complete with the result data. Could you clarify the exact role of callback_url? Is it the 3DS notification URL (where ACS POSTs within the iframe context after DDC/challenge completes)? Or is it a server-to-server webhook? Understanding this will help implementors know what kind of endpoint to expose.
There was a problem hiding this comment.
Fixed the sequence to make it clear that it's the frontend callback. Also added a note at the end of the sequence.
There was a problem hiding this comment.
Perhaps we could name this field more clearly to remove some confusion as well.
There was a problem hiding this comment.
I have created a separate json for client_callback, which includes url and status. This makes way for a clean representation of server_callback for webhooks when we need it.
| } | ||
| }] | ||
| }, | ||
| "signals": { |
There was a problem hiding this comment.
Richer browser signals increase the probability of frictionless authentication and improve conversion. How does the host know what signals are required or supported? Should the payment handler's published schema advertise the required/optional browser signals, so hosts know what to collect?
There was a problem hiding this comment.
I have expanded the risk section to include the signals details and the signals.json is now updated with the details.
| ```json | ||
| { | ||
| "id": "chk_123", | ||
| "status": "completed", |
There was a problem hiding this comment.
After the DDC and challenge flow completes, the final response returns status: "completed" with an order but no authentication metadata. Should there be an optional field indicating the authentication outcome (e.g., authenticated, attempted, frictionless, not required)? Hosts that participate in rendering the challenge may need this for observability, metrics and compliance.
There was a problem hiding this comment.
Added a new field called challenge.auth_outcome for this purpose.
|
|
||
| * **Standard Flow Execution:** If the challenge interaction completes successfully (via the hidden iframe or popup) and the Host receives the result data, it **MUST** resume the completion flow by calling `Complete Checkout` again with the received data. | ||
| * **Backend Validation Failure:** If the data returned from the challenge is invalid or the authorization fails at the PSP level, the Merchant will return a terminal error response (or another status) in the subsequent `Complete Checkout` call. The Host should handle this as a standard checkout error. | ||
| * **User Abandonment / Interactive Errors:** If the user enters an error state within the challenge UI or cannot proceed (e.g., the user manually dismisses the challenge popup before completion), this **SHOULD** be treated as a terminated session. The current checkout session becomes invalid for completion, and the user must initiate a new checkout session to try again. |
There was a problem hiding this comment.
Does abandonment have to terminate the entire checkout session? The 3DS session at the ACS is dead, but could the checkout session remain retryable (host calls /complete again, merchant initiates a fresh 3DS attempt)? This would allow buyers who accidentally dismissed the popup (or for some reason popup failed to render) to retry without losing their cart, shipping selections, etc.
There was a problem hiding this comment.
Changes made to the flow and error handling sections to incorporate the recoverable errors and how users can switch/retry in such scenarios.
|
|
||
| When processing challenge actions, the Host must consider security and rendering requirements: | ||
|
|
||
| * **Trusting the URL:** The `url` provided in the `action` object is generated by the PSP (or 3DS Provider). While validating the trustworthiness of this URL is out of scope for the UCP specification, the Host **SHOULD** take appropriate measures to ensure it only loads trusted endpoints (e.g., verifying against known PSP domains or using browser security policies). |
There was a problem hiding this comment.
For the host to verify trust on the url in the action, the payment handler's published schema (hosted at a PSP-owned URL, independently fetchable by the host) could declare allowed URL patterns for challenge actions. This way the host has a verification source independent of the merchant.
There was a problem hiding this comment.
Thanks for the suggestion. Updated the section.
| "type": "object", | ||
| "description": "Display information for this payment instrument. Each payment instrument schema defines its specific display properties, as outlined by the payment handler." | ||
| }, | ||
| "challenge": { |
There was a problem hiding this comment.
I'm thinking we should make this an array of challenges so it's generic enough going beyond 3DS, if an issuer is able to support multiple challenges including 3DS/SMS/Email etc. @prasad-stripe please share your thoughts.
There was a problem hiding this comment.
Thanks for raising this. My instinct is to keep it as a single object for now. In practice, a completed 3DS is a stronger signal than completed SMS/email challenge from risk point of view, so the UCP layer doesn't need to model that multiplicity.
If we were to make this an array, we'd need to define sequencing semantics (all? any? ordered?) which is a significant design surface without real use cases driving it yet. I'd suggest we note in the spec that multi-challenge support is a known future extension point, but keep the current single-object shape to avoid premature complexity.
| "deprecated": true, | ||
| "examples": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"] | ||
| }, | ||
| "dev.ucp.browser_info.accept_headers": { |
There was a problem hiding this comment.
A couple of comments/questions on the proposed signal changes here:
- How will
browser_info.buyer_ipandbrowser_info.user_agentdiffer from the existing ones we currently have? Any reason why we can't lean on these 2 signals as-is and only introduce the net new signals?
1.1. If for any reason we want to consolidate everything intobrowser_info, like how you are deprecating the other signals, then we should do it consistently and also use the UCPtransitionschema to move it to "omit" so we can drop it in the next version:
"transition": {
"from": "optional",
"to": "omit",
"description": "..."
}
- Any particular reason for the
browser_infowrapper around all the individual signals (I don't think EMVCo requirements specifically required the wrapper)? If we feel wrapper is the right way to go, have we consider just defining 1 object intypes/to contain all these browser signals? i.e.
"dev.ucp.browser_info": {
"$ref": "brower_info_signals.json"
}
There was a problem hiding this comment.
Thanks Jing.
- How will
browser_info.buyer_ipandbrowser_info.user_agentdiffer from the existing ones we currently have? Any reason why we can't lean on these 2 signals as-is and only introduce the net new signals?
1.1. If for any reason we want to consolidate everything intobrowser_info, like how you are deprecating the other signals, then we should do it consistently and also use the UCPtransitionschema to move it to "omit" so we can drop it in the next version:"transition": { "from": "optional", "to": "omit", "description": "..." }
I was looking at having a logical grouping of signals. There can be other signals such as device signals, which will be different from the browser signals, so it is better not to have a flat list for all signals. I can make the changes to add transition but wondering if there are any guidelines I should follow to do that.
- Any particular reason for the
browser_infowrapper around all the individual signals (I don't think EMVCo requirements specifically required the wrapper)? If we feel wrapper is the right way to go, have we consider just defining 1 object intypes/to contain all these browser signals? i.e."dev.ucp.browser_info": { "$ref": "brower_info_signals.json" }
I have moved browser signals to a separate file as suggested.
| Business-->>Platform: complete_in_progress (Challenge Action) | ||
| deactivate Business | ||
|
|
||
| Note over Platform: Render Challenge UI (Popup) |
There was a problem hiding this comment.
Naive question on this: At a high-level, this feels like we are initiating an embedded context that renders/embeds the 3DS challenge on platform host. Have we considered a design option to augment our existing embedded transport to support this use case (i.e. I guess in this case it would be between platform host and PSP issuer) so we don't need to introduce a new pattern of exchanging data via callback_url..?
There was a problem hiding this comment.
IIUC, 3DS is a problem that doesn't exist in the context of embedded transport. This is because the merchant is able to embed its own UI which means it will be able to render the 3DS flows also if required by the issuer. So this is a problem we need to solve for the native integration, hence this path is followed.
| ## UCP Spec Enhancement | ||
|
|
||
| In this model, the **Business** acts as the orchestrator, and the **Platform** acts as the execution environment for standardized 3DS2 actions. | ||
| The design uses `complete_in_progress` to signal that the checkout is in an intermediate state, awaiting the result of a 3DS2 action. |
There was a problem hiding this comment.
On top of this signal, would it be good to consider whether there should be an accompanying message so platform can clearly know what to parse-for/look-at in case the complete_in_progress status is used in other non-3DS challenge context?
There was a problem hiding this comment.
Thanks for the suggestion, I have made changes to include a new code called challenge_required for this purpose.
| "intent": "challenge", | ||
| "type": "popup", | ||
| "url": "https://bank-acs.com/challenge", | ||
| "request_data": { "creq": "eyJtZXNzYWdlVHlwZSI6..." }, |
There was a problem hiding this comment.
Apologies for my unfamiliarity on how 3DS works, but curious about this field: based on the schema description below, it sounds like this is Data to be sent with the request. when platform loads the url from above, do we have restrictions around what can be in here? If it's truly a freeform, is there any concerns around injection attacks when a compromised business is involved..?
There was a problem hiding this comment.
This is expected to be free form since every PSP will have it's own way of handling it. Under URL security, I have added the considerations around trusting the URL, and the trust of the payload should go along with it.
|
|
||
| To prevent the Business from sending challenge actions that the Platform cannot execute (e.g., in non-browser environments), the Platform should be able to signal its capability to handle challenges upfront. | ||
|
|
||
| - **Capability Signal:** If the Platform does not declare support for handling challenges (e.g., via a generic delegation negotiation mechanism), the Business **MUST NOT** send a `complete_in_progress` status with a challenge action. |
There was a problem hiding this comment.
This probably should be done as part of platform profile (some options: should be a config under payment_handler or checkout capabilities).
There was a problem hiding this comment.
Pushing back on the need to declare challenge support & statement of "the Business MUST NOT send a complete_in_progress status with a challenge action." -
Maybe controversial but if the platform can't execute a challenge the PSP returned, that's the platform's problem, not the business's. Platforms that never have a way to render a challenge UI shouldn't advertise support for handlers whose action surface includes challenges. So this would already be covered by existing handler discovery. The handler spec implicitly declares its action surface (including whether 3DS challenges may be issued) by stating it supports cards. A platform that can't render challenges should self-select out of those handlers and advertise only ones whose action surface it can execute end-to-end (ACH, SEPA, wire transfer, stored-token MIT flows, etc.). So I don't think we need explicit declaration of challenge support from the business or platform.
A 3DS challenge isn't something the business can choose — it's controlled by the PSP and ultimately the issuer based on risk scoring, issuer policy, and (for EEA cards) PSD2/SCA mandates. A business "advertising payment_handler support for cards without 3DS" is effectively advertising a payment method that will get declined by European issuers (and has a chance of getting declined in the US, Canada, Australia, etc). The merchant is a pass-through — the PSP returns an action or it doesn't, and the merchant forwards it.
The business has no alternative PSP path — there's nothing meaningful to do in the adapter besides forward the action. Requiring the business to add conditional logic which would check if the platform said they support 3DS before they respond with the PSP’s challenge adds unnecessary complexity and is solving for a decision that doesn't exist, and the failure mode it would prevent (buyer can't complete a challenge) is the same as any other graceful payment failure: the challenge doesn't complete, the PI fails, the buyer picks another method or abandons.
There was a problem hiding this comment.
I agree with the POV that this can be achieved at the payment_handler config level. But we will still have a problem where it could lead to a dead end if the Platform is not able to choose any payment handler. Instead of treating this as WAI, I was wondering if there is some way to indicate this issue to the business so they can provide the requires_escalation response. Then they can themselves handle the flow. Are there scenarios in UCP currently where we run into a similar problem, where we guide the business to fallback to requires_escalation? If there is a precedent, we could follow that.
There was a problem hiding this comment.
If there is no compatible payment handler between a Platform and Business, that should be handled during capability negotiation, which occurs well before /complete. In that case, the buyer should be escalated to the Business-hosted flow via continue_url, avoiding a runtime dead-end.
Once a Platform proceeds into /complete for a handler, it should be treated as a commitment that the Platform can execute that handler’s runtime contract (including non-happy-path behavior like 3DS). Otherwise, we shift platform capability risk onto the Business at runtime, which effectively makes the Business a compatibility layer for contracts it doesn’t own.
Do you agree with that ownership split?
There was a problem hiding this comment.
I suspect many of these ownership ambiguities come from treating 3DS as an add-on to card-capable payment_handlers (including wallets like Google Pay). For card methods, 3DS should be part of the baseline handler runtime contract. Once framed that way, capability negotiation and fallback ownership become much clearer.
There was a problem hiding this comment.
Thanks Julia, agreed on this ownership split. I have modified the section to reflect that.
| The business remains the Merchant of Record (MoR), and they don't need to become | ||
| PCI DSS compliant to accept card payments through this Capability. | ||
|
|
||
| For handling 3DS2 challenge flows in a vendor-agnostic manner, see the [Checkout 3DS2 Guide](checkout-3ds2-guide.md). |
There was a problem hiding this comment.
Should we consider adding this under the Complete Checkout operation section - https://ucp.dev/latest/specification/checkout/#complete-checkout..?
There was a problem hiding this comment.
Moved under completeCheckout.
| "format": "uri", | ||
| "description": "The URL where the result should be sent or where the interaction completes." | ||
| }, | ||
| "action": { |
There was a problem hiding this comment.
In the case of a 3DS challenge the action request is coming from the PSP (The business is mostly acting as a pass through for the payment data). As such, I am confused by some of the proposed properties/the structure here. Take "type" for example - the business is not determining how the action from the PSP should be rendered by the platform as they are not in control of the action request. I am worried this proposal suggests that the business take on the role of translating PSP specific logic into boxes it doesn't naturally fit in and the business doesn't have control over.
For example, when a merchant's Adyen integration receives an action from a payment request today, the merchant server forwards that object verbatim to the Adyen Web Drop-in SDK on the client. The SDK reads its known shape and decides whether to render a fingerprint via hidden iframe, a challenge via visible iframe, a redirect, an SDK flow, a voucher, a QR code, an await poll, etc. The merchant server never interprets the action — that's the SDK's job. Now with UCP, the protocol is essentially asking "what if the platform is the client?" and the natural answer is "then the platform should consume the PSP's action the same way the client SDK does today."
UCP already has a precedent for this exact pattern: payment_handler.config. A handler declares some UCP-typed top-level metadata (type, version, schema, etc.) and an opaque config whose shape is defined by the handler's own published schema. UCP doesn't have an opinion about what's inside config. Could challenge follow the same shape? Pull the things that genuinely don't vary by PSP up to the challenge envelope, and let action (and result) be opaque PSP-payload bags? See example below
"challenge": { "callback_url": "https://platform.example/cb/abc", "intent": "device_data_collection", "display": "hidden", "expires_at": "...", "action": { // PSP's action object, passed verbatim from PSP response. // additionalProperties: true — UCP has no opinions on shape. "type": "threeDS2", "subtype": "fingerprint", "token": "...", "paymentData": "...", "url": "..." } }
Note: I replaced your suggested type field with display — hidden vs visible (keeps rendering vocabulary out of the protocol while still letting the platform pick its rendering primitive)
There was a problem hiding this comment.
Thanks Julia.
The lift on the business side is expected to be minimal and we do expect them to just pass through the response from the PSP. Regarding the actual implementation of the rendering, it might be done by the platform itself or it may rely on the payment handler to do so. I'm not sure if making some fields explicit and some not will solve the problem. Whoever is rendering needs to understand the full action payload. If the specifics of the action payload is not well-defined, it can lead to a heavy lift between payment handlers and PSPs (many-to-many) and could lead to fragmented implementation. So I think we have 2 options here.
- Make the action payload standardized (what's defined in the current PR).
- Make it generic but provide clear guidance on how the payload should look like in a standard implementation. It will help guide PSPs and Payment handlers handle the functionality without needing too much co-ordination. There could be exceptions but at least for the normal functionality, such a guidance can prove to be beneficial.
Please share your thoughts.
There was a problem hiding this comment.
Thanks for getting back to me @aneeshali — and I completely agree with your point that whoever is doing the rendering needs to understand the full action payload. That said, I think there is tension between this PR and the statement that “the lift on the business side is expected to be minimal” and that businesses can “just pass through the response from the PSP.”
If the action payload is standardized, the business is no longer doing a true pass-through. It has to add a normalization/translation layer mapping PSP-native payloads into UCP fields. Businesses already have production PSP integrations and existing challenge flows, so this introduces new transformation logic on a highly sensitive payment path.
It also pushes change-management to the wrong owner: businesses don’t control PSP action contracts, but would have to continuously adapt to PSP payload evolution and version drift specifically for UCP. That increases maintenance and correctness risk where failures directly impact conversion and payment reliability.
I think we should standardize cross-PSP lifecycle/state semantics (e.g., complete_in_progress, callback handshake, timeout/retry semantics, transition ownership), but keep action/result PSP-shaped and opaque.
If a Platform advertises support for a handler, it should own the runtime parsing/rendering and change-management for that handler’s challenge semantics (like 3DS), while the Business remains a pass-through/orchestrator at the PSP boundary.
There was a problem hiding this comment.
Thanks for sharing the context. Yes it makes more sense to keep the action and action_result payloads generic, so it works across the board. I have updated the schema to reflect that.
213b01c to
281ba1b
Compare
281ba1b to
4110e4a
Compare
drewolson-google
left a comment
There was a problem hiding this comment.
Added a few comments, overall looks good.
| "token": "examplePaymentMethodToken" | ||
| }, | ||
| "challenge": { | ||
| "callback_url": "https://p.g.com/challenge/callback/session_123" |
There was a problem hiding this comment.
Perhaps we could name this field more clearly to remove some confusion as well.
|
|
||
| #### Step 2.2: Platform Submits DDC Result (Platform -> Business) | ||
|
|
||
| The Platform renders the hidden iframe. Once it completes, the Platform resumes the complete call. |
There was a problem hiding this comment.
Rendering a hidden iframe assumes the Platform is running in a context that can render an iframe. Is it possible to keep the spec more generic and be less specific about the mechanisms for device data collection?
There was a problem hiding this comment.
I agree iframe is too specific. I have updated the guide to call it executing action, with iframe/popup etc being examples.
| "challenge": { | ||
| "result": { | ||
| "intent": "challenge", | ||
| "status": "abandoned" |
There was a problem hiding this comment.
Is this modeled as a free-form text field or an enum type?
There was a problem hiding this comment.
Thanks Drew for pointing out. I have converted this field and other relevant fields to enums.
| ] | ||
| }, | ||
| "status": { | ||
| "type": "string", |
There was a problem hiding this comment.
Why is this a string and not an enum? What should the recipient do with unrecognized strings?
| | `risk_rejected` | `recoverable` | The transaction was rejected due to high risk for this specific instrument. The Platform should guide the buyer to use a different payment instrument. | | ||
| | `payment_timeout` | `recoverable` | The ACS or 3DS server timed out. The Platform may allow the user to retry the operation or guide them to switch to a different payment instrument. | | ||
|
|
||
| ### Abandonment and Instrument Switching |
There was a problem hiding this comment.
Can you rework this section to explicitly define state-transition behavior for the new checkout states introduced by this PR?
Specifically, what is the expected business behavior when a second POST /complete arrives with a new payment instrument while the checkout is already in complete_in_progress?
- Should the business always move
complete_in_progressback toready_for_completebefore processing the second /complete? - Or only when the second request includes the old instrument marked
selected: falsewithchallenge.result.status: "abandoned"? - If that old-instrument payload is missing (and that section currently says platform SHOULD, not MUST), should the business reject the second /complete to avoid ambiguous behavior?
I’m calling this out because transitioning from complete_in_progress back to ready_for_complete may involve non-trivial business-side work (e.g., failing a partial order back to basket, voiding pending PSP authorization, and restarting the complete flow with the new PI). Without explicit normative guidance here, it introduces risk for the business and different implementations may handle retries/switches inconsistently.
There was a problem hiding this comment.
Thanks Julia. I have added more details to clarify the section as suggested. Changed to MUST and gave the explanation of why the status can stay at complete_in_progress
4110e4a to
ed28aa0
Compare
ed28aa0 to
0b7b243
Compare
0b7b243 to
4be562d
Compare
raginpirate
left a comment
There was a problem hiding this comment.
@aneeshali — appreciate the work, especially the EMVCo signals shape and the lifecycle thinking. Want to push on layering before this settles. I think a deep dive would help a lot.
The structural concern: this PR makes platforms couriers for 3DS protocol context (transStatus, CRes payloads, success/failure interpretation) that feels off crossing the UCP wires; it creates a dependency on platforms to build robust, 3ds specific implementations and for businesses to change their usually simple redirect URL integration with PSPs into a much more complex one.
I've been drafting a generic actions primitive on the checkout response — the business emits an imperative to render a URL in a specified form (invisible | inline | modal | full_page | redirect), with simple embedded protocol-like responses and results written back to a result_path. I think this could be a much simpler, more extensive path to get things like ddc and 3ds2 supported, without increased complexity for all parties when compared to simple checkout integrations.
Two implications for 3DS:
- Business-owned frame URLs. Platform mounts merchant.com/3ds/ddc, not acs.issuer.com/.... The ACS interaction happens inside that wrapper; the 3DS payload flows ACS → business server-to-server; the platform's postMessage back is just to have the frame closed.
- DDC can be done early in update_checkout. It's invisible data prep, not a submit-time step. Severity optional, mounted in the background as soon as the instrument is selected — buyer never waits on it. Submit time can also handle it as well, but hopefully folks can complete it as early as checkout loads with vaulted credentials.
Sequence diagram showing my early thoughts
And PR (please note theres lots to dislike about this AI slop, but it might be helpful to speak to with your favourite agent to decipher the intent here): #458
WDYT about shifting from solving 3ds to solving a generic frame-communication standard for UCP, and keeping 3ds, ddc, captchas, and 3p methods as follow-up implementations built ontop?
|
Thanks for the feedback, @raginpirate. Sounds good, we can move the discussion to #458. I was wondering how we could do a deep dive. If you meant syncing on the PR, that works too. Look forward to getting this addressed soon. |
|
One source-contract readback before this lands:
|
prasad-stripe
left a comment
There was a problem hiding this comment.
Thanks for the updates @aneeshali, the flow is much clearer now with the client_callback separation, enum conversions, and the error handling/recovery section. Good progress. I've left a few inline comments on areas that I think need attention.
| "timezone": { | ||
| "type": "string", | ||
| "description": "Browser Time Zone offset or name.", | ||
| "examples": ["America/Los_Angeles", "-480"] |
There was a problem hiding this comment.
The EMVCo AReq field (browserTZ) is specifically the UTC offset in minutes (e.g., "-300" for UTC-5). This value is trivially available client-side via getTimezoneOffset(). Allowing both a timezone name ("America/Los_Angeles") and an offset in the same field creates ambiguity for the business/PSP when mapping to the AReq. Suggest making this an integer offset field, and if there's a use case for timezone name, adding it as a separate optional field.
| "signals": { | ||
| "dev.ucp.browser_info": { | ||
| "user_agent": "...", | ||
| "color_depth": 24 |
There was a problem hiding this comment.
This should be string according to: https://github.com/Universal-Commerce-Protocol/ucp/pull/421/changes#diff-4bee59bbd434f780cb6676f38a32470f5510f82055fec48f634b37c491de4a75R31
| "format": "uri", | ||
| "description": "The URL where the result should be sent or where the interaction completes." | ||
| }, | ||
| "action": { |
There was a problem hiding this comment.
An absolute timestamp requires clock synchronization between business and platform, which is unreliable. For the typical 5-10s DDC timeout, an interval (e.g., timeout_seconds: 10) is more practical. It tells the platform how long to wait after posting to the 3DS Method URL, without needing synchronized clocks. WDYT?
|
|
||
| ### Sequence Diagram | ||
|
|
||
| ```mermaid |
There was a problem hiding this comment.
Would be great to add AReq/ARes and RReq/RRes to the diagram. These are the authoritative protocol messages and showing them helps implementers understand what's happening on the business/PSP side when the platform is waiting.
| "description": "The outcome of the authentication flow (e.g., 'authenticated', 'frictionless', 'attempted'). Optional, for platform observability and metrics.", | ||
| "enum": [ | ||
| "authenticated", | ||
| "frictionless", |
There was a problem hiding this comment.
"frictionless" is a flow path (no challenge was presented), not an authentication outcome. The other values here (authenticated, attempted, failed) map to protocol results, but frictionless describes how we got there rather than what happened.
Additionally, the current enum doesn't cover several transStatus values from the protocol. For reference, the EMVCo transStatus values (3DS Protocol and Core Functions Spec, Table A.5):
| Value | Meaning |
|---|---|
| Y | Authentication successful |
| N | Not authenticated / denied |
| U | Authentication could not be performed |
| A | Attempts processing performed |
| C | Challenge required |
| R | Authentication rejected |
| I | Informational only |
With the current enum, there's no distinction between N and R (both collapse into "failed"), and I and U have no representation at all. These have different risk implications for the platform/business.
I will suggest either mapping these more directly to protocol outcomes (with a clear mapping table from transStatus values) or renaming this field to something like auth_flow_result to signal it's a UCP-level abstraction and documenting the mapping.
This PR adds vendor-agnostic 3DS2 support to the UCP specification and schemas. Closes #420.