v139 · Web APIs · Payments
SPC Flow Walkthrough
Step through each phase of a Secure Payment Confirmation transaction — from the merchant checkout page to the refreshed Chrome dialog to the returned authentication signature.
Step 1 — Merchant checkout
shop.example · secure checkout
Mechanical keyboard$89.00
USB-C cable ×2$12.00
Shipping$5.00
Total$106.00
What happens next: clicking "Pay with saved card" calls
new PaymentRequest() with the secure-payment-confirmation method. The browser checks for a registered passkey credential matching this user.
merchant JS
// On "Pay" click, the issuing bank's
// script constructs a PaymentRequest.
// The challenge is issued by the bank,
// not the merchant.
document.querySelector('.pay-btn')
.addEventListener('click', async () => {
const challenge = await bank.getChallenge(
userId, amount
);
await initiateSPC(challenge, amount);
});
Step 2 — Authentication request
The issuing bank (running in an iframe or redirect) constructs a PaymentRequest with the secure-payment-confirmation payment method. Key fields drive the refreshed dialog UI.
| API field | Dialog element |
|---|---|
instrument.displayName | Card name heading |
instrument.icon | Card brand icon |
total.amount | Large amount display |
payeeOrigin | Verified merchant origin |
challenge | Signed in signature response |
bank iframe JS
const request = new PaymentRequest( [{ supportedMethods: 'secure-payment-confirmation', data: { credentialIds: [credBytes], challenge: challengeBytes, instrument: { displayName: 'Visa ···· 4242', icon: 'https://bank.ex/logo.png', }, payeeOrigin: 'https://shop.example', timeout: 60000, }, }], { total: { label: 'Order total', amount: { currency: 'USD', value: '106.00', }, }, } );
Step 3 — Chrome dialog (simulated)
shop.example
https://shop.example · verified
amount due
$106.00
Visa ending 4242
···· ···· ···· 4242
Authenticate with Touch ID / Face ID
Before Chrome 139
- Dense text layout
- Small instrument icon
- Amount buried in list
- Payee URL hard to read
- Biometric as separate step
Chrome 139+
- Icon-forward merchant header
- Large card icon, prominent name
- Amount in large display type
- Origin verified + visible
- Biometric integrated in dialog
dialog render
// The browser renders the dialog from // the PaymentRequest data fields. // No merchant JS needed after .show(). const response = await request.show(); // Chrome 139: dialog shows // - merchant origin + icon // - instrument.displayName // - instrument.icon (larger) // - total.amount (prominent) // - integrated biometric prompt // API surface is unchanged from v138 — // only the dialog visual is refreshed.
Step 4 — Signature returned
After biometric verification, request.show() resolves with a PaymentResponse. The details.signature is a CBOR-encoded WebAuthn assertion.
type: "payment.publicKey"
id: "AbCdEfGhIj…" (credential ID)
details.signature: ArrayBuffer (CBOR) ← proves intent
details.authenticatorData: ArrayBuffer
details.clientDataJSON: { type: "payment.get", challenge: "…", origin: "https://bank.ex", payment: { total: { currency: "USD", value: "106.00" } } }
What it proves: the
signature is over a payload that includes the challenge, the origin, and the payment details. The bank verifies the signature server-side to confirm the user authenticated this exact transaction — amount and payee — not a replayed credential.
bank verifies response
const response =
await request.show();
const {
signature,
authenticatorData,
clientDataJSON,
} = response.details;
// Send to bank server for verification:
await fetch('/api/verify-payment', {
method: 'POST',
body: JSON.stringify({
id: response.requestId,
signature:
bufToBase64(signature),
authenticatorData:
bufToBase64(authenticatorData),
clientDataJSON:
bufToBase64(clientDataJSON),
}),
});
// Bank verifies the WebAuthn assertion
// including the payment context —
// prevents replay and amount tampering.