JavaScript Payment Request API Table

PieceWhat it doesField note
new PaymentRequest(...)The objectMethods declare wallets, details carry total + line items - built per checkout, not per session
request.show()The gesture gateNeeds transient user activation - build AND call inside the click handler or it rejects
canMakePayment()The quiet checkRate-limited for fingerprinting - once per session, cached; false = fallback checkout, not broken user
shipping eventsThe address flowshippingaddresschange blocks the sheet until you resolve updateWith - your latency budget is visible
response.complete()The closingSheet spins until called - success only after your SERVER charged the token, never before
Apple Pay on SafariThe real methodPR is Safari's only web wallet: https://apple.com/apple-pay method + server merchant validation
the wallet takeoverThe honest arcWallets won the button, PR won the form - Google Pay rides the sheet, Chrome dropped handlers
request.abort()The escapeYour timeout/UX exit; user dismissal rejects with AbortError - both must restore the buy button
Reference: the MDN Payment Request API. The API is the browser's native checkout sheet: you describe methods and a total, the browser renders its trusted UI with the user's stored details, and resolves with a token - a UI standard, not a money mover.
Bottom line: two rules decide everything. show() demands transient user activation - construct and call it inside the click handler, because a sheet shown from a timer is a rejected promise. And show() resolving is authorization, not payment: the server exchanges the token with the wallet before complete('success') - completing before the server confirms means a crafted client can skip the charge. The honest arc: wallets won the button, Payment Request won the form - declare Apple Pay (Safari's only web path), Google Pay and a classic fallback, and let the sheet arbitrate.
Related tools: the fullscreen table (the other transient-activation API), the fetch table (the server confirmation leg), the permissions table (where the platform gates live), and the form validation table (the fallback checkout you keep for no-wallet users).

The Payment Request API is the browser's native checkout sheet: you describe payment methods and a total, the browser renders its own payment UI (wallets, cards, contact details it already has), and resolves with an authorized response your server still has to verify. No checkout form, no card-entry UX of your own - the object is three arguments: methods (which wallets), details (the total and line items), and options (what you need from the user).

Bottom line: two rules decide whether it works at all. show() demands transient user activation - build and call the request inside the click handler, or it rejects - and show() resolving is NOT a payment: it means the user approved the sheet, and your server now charges the token through the wallet's server-side flow before you call response.complete('success'). Skip the server step and you have built a demo; call complete() late and the browser's spinner hangs over your confirmation page.

The honest part: Payment Request is the rail, not the train. Chrome deprecated its payment-handler ambitions, and the wallets people actually have - Apple Pay, Google Pay, LINK - ride through it as payment methods (Apple Pay is the ONLY web wallet path on Safari, expressed as a https://apple.com/apple-pay method with its own data shape). What you gain is the sheet and the user's stored details; what you still own is the merchant integration, the validation, and the refund story.

How to use

  1. Build inside the gesture: on click, construct new PaymentRequest([{supportedMethods: 'basic-card'}], {total: {label: 'Total', amount: {currency: 'USD', value: '29.00'}}}) and call show() immediately - the transient-activation window closes fast, and a request shown from a timer is a rejected promise.
  2. Use canMakePayment sparingly: it answers whether ANY declared method can run here, which lets you choose between the native sheet and a fallback button - but browsers rate-limit it for fingerprinting reasons, so call once per session, cache the answer, and never on every render.
  3. Wire the shipping events: request options requestShipping: true plus shippingoption id; handle shippingaddresschange and shippingoptionchange by validating (reject impossible addresses), recomputing tax and shipping, and resolving event.updateWith(newDetails) - the sheet blocks until you resolve, which is your latency budget.
  4. Close the loop with complete(): response from show() carries method details (card or wallet token); send it to your server, charge through the wallet's server API, then call response.complete('success') - or 'fail' to tell the browser to show failure. The sheet stays 'processing' until you call it, so slow server calls need optimistic UI elsewhere.
  5. Handle both exits: user dismissal rejects show() with AbortError (catch it and reset the button - it is the normal 'changed my mind' path, not an error), and your own request.abort() cancels from your side (stale carts, timeouts). Both exits must restore the buy button or the checkout dead-ends silently.

Frequently asked questions

Why does request.show() throw NotAllowedError outside a click handler?

Transient user activation: the browser grants each user gesture a short-lived 'activation budget', and opening a native payment sheet - high-stakes UI that looks browser-trusted - is gated on it. The activation expires after seconds or after the first consuming call, so a PaymentRequest built on page load or shown from a setTimeout/fetch callback arrives at show() with no activation left. The fix is structural, not a retry: construct AND show inside the click handler; if you must fetch a total first, fetch on click and show after, or prefetch totals into a cache the click reads instantly. The same activation gate governs fullscreen and file picking - it is the platform's anti-hijack pattern.

Is canMakePayment() a 'does this user have Apple Pay' check?

Loosely, and the looseness matters. It answers whether any declared payment method is plausibly available on this device - Apple Pay on Safari with a card provisioned, basic-card on others - but it is deliberately coarse and rate-limited because the answer itself is a fingerprint (knowing a user HAS a wallet is identity data). Practical discipline: call once per session, cache it, and treat false as 'show the regular checkout' not 'user is broken'. And never use it as an analytics signal for wallet ownership - the rate limit will quietly return garbage once you cross it, which is the browser telling you to stop.

Do I still need a payment processor with Payment Request?

Yes - more than ever. The sheet collects and authorizes; it never moves money. The response carries a method-specific payload (a card for basic-card, an Apple Pay payment token, a Google Pay token), and your SERVER exchanges that with the wallet/processor (Stripe, Adyen, the wallet's own API) for an actual charge - including the server-side verification that the token was minted for YOUR merchant. The classic production mistake is trusting the client resolution: response.complete('success') before the server confirms means a crafted client can skip the charge entirely. The API is a UI standard; the money path is your backend's.

What happened to Payment Request vs the wallets?

The 2016 pitch was universal browser checkout replacing wallet buttons; the 2026 reality is wallets won the identity layer and Payment Request became their rail. Chrome dropped its payment-handler apps and routes Google Pay through the sheet; Apple never built anything else for the web - on Safari, Payment Request IS Apple Pay, declared via the https://apple.com/apple-pay method with merchant validation on your backend. So the modern integration is: declare the wallet methods you support, let the sheet arbitrate, fall back to classic checkout for no-wallet users. The API lost the war for the button and won the war for the form.

Related tools