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.displayNameCard name heading
instrument.iconCard brand icon
total.amountLarge amount display
payeeOriginVerified merchant origin
challengeSigned 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)

chrome — secure payment confirmation
S
shop.example
https://shop.example · verified
amount due
$106.00
VISA
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.

see also