Skip to content

Capabilities · API

Payment Request API

Published

Limited availabilityNot supported in Firefox (Desktop)W3C

PaymentRequest hands the checkout sheet to the browser: the page lists the payment methods it accepts, the line items and total, and which contact fields it needs, and the browser collects a credential from a stored card, Apple Pay, Google Pay, or an installed payment handler and returns it as a PaymentResponse. The page never renders a card form and never sees raw card data unless the chosen method returns it.

Chrome 60 on desktop and Chrome 53 on Android shipped the constructor, Edge 15 and Safari 11.1 followed, and Android WebView added it in 136 (BCD api.PaymentRequest). Safari implements it for one method only, https://apple.com/apple-pay. Firefox 55 shipped an implementation behind dom.payments.request.enabled and dom.payments.request.supportedRegions and has not enabled it by default in any release.

new PaymentRequest(methodData, details)
new PaymentRequest(methodData, details, options)
request.canMakePayment()
request.show()
request.show(detailsPromise)
request.abort()
response.complete()
response.complete(result)
response.retry(errorFields)

canMakePayment() returns a Promise<boolean> without showing UI. show() returns a Promise<PaymentResponse> that fulfils when the user authorises payment; abort() and complete() return Promise<undefined>, and retry() returns a promise that fulfils when the user has corrected the flagged fields. The constructor is [SecureContext] and requires the payment Permissions Policy, whose default allowlist is 'self'; a cross-origin iframe needs allow="payment". A request object is single-use: after show() has been called once its [[state]] leaves "created", and a second show() or a later canMakePayment() rejects.

The constructor takes two required dictionaries and one optional one.

Parameter Type Required Description
methodData sequence<PaymentMethodData> Yes One entry per accepted method. supportedMethods is a payment method identifier, either a URL such as https://google.com/pay or https://apple.com/apple-pay or a standardised string such as secure-payment-confirmation; data is an object the method’s own specification defines (merchant ids, allowed networks). The list must not be empty or contain the same identifier twice.
details PaymentDetailsInit Yes total (PaymentItem: label, amount) is required; amount is a PaymentCurrencyAmount with an ISO 4217 currency code and a decimal string value such as "29.99". Optional members: id (generated as a UUID when omitted), displayItems, shippingOptions (id, label, amount, selected), and modifiers (supportedMethods, total, additionalDisplayItems, data) that change the totals for one method.
options PaymentOptions No requestPayerName, requestPayerEmail, requestPayerPhone, requestShipping (booleans, default false) and shippingType ("shipping", "delivery", or "pickup", default "shipping"), which only changes the label the sheet shows.

show(detailsPromise) accepts an optional Promise<PaymentDetailsUpdate> so the sheet can open before the server has finished pricing the cart. complete(result) takes "success", "fail", or "unknown" (the default). retry(errorFields) takes a PaymentValidationErrors dictionary with error, payer (name, email, phone), shippingAddress (an AddressErrors record keyed by address field), and paymentMethod.

The specification defines the following failures. Where Chrome attaches a fixed message it is quoted from components/payments/core/error_strings.cc.

Exception Condition
SecurityError Constructor: the document is not allowed to use the payment Permissions Policy. show(): the browser requires transient activation and finds none, or rate-limits the call.
TypeError Constructor: methodData is empty; details.total is missing; an amount.value is not a valid decimal monetary string or the total is negative; two shipping options share an id. Chrome adds limits of 1024 shipping options, 1024 modifiers, and 1024 characters for details.id.
RangeError Constructor: a payment method identifier is not a valid URL or standardised string, the same identifier appears twice, or a currency is not a well-formed ISO 4217 code.
InvalidStateError show(): the document is not fully active, or [[state]] is not "created" (Chrome: Already called show() once). abort(): no show() or retry() is in progress, an abort() is already pending, or the browser cannot abort the current interaction. canMakePayment(): [[state]] is not "created" or the document is not fully active. complete() and retry(): the response is already complete or a retry() is pending.
AbortError show(): the document is not visible, another payment sheet is already showing (Chrome: Another PaymentRequest UI is already showing in a different tab or window.), the user dismisses the sheet (Chrome: User closed the Payment Request UI.), or the page calls abort() (Chrome: The website has aborted the payment). complete(): the document stopped being fully active while the sheet was open.
NotSupportedError show(): no listed method is supported. Chrome also rejects on origins that are neither localhost, file://, nor a cryptographic scheme with Only localhost, file://, and cryptographic scheme origins allowed.
NotAllowedError canMakePayment(): the browser applies an anti-fingerprinting quota to repeated queries with varying method lists.
OperationError show(): the selected payment handler reports an internal error, for example the OS terminated it.

canMakePayment() resolving false is not an exception: it means no listed method is available on this device, which is the signal to show a hosted checkout instead.

The examples use Google Pay as the method identifier; swap in your processor’s identifier and data object. Each one checks for the constructor first and falls back to a hosted checkout page when it is missing or no method is available.

Showing a wallet button only when canMakePayment() resolves true

Section titled “Showing a wallet button only when canMakePayment() resolves true”

Build the request at page load, ask whether any listed method is available, and reveal the native button only on true. A false result or a missing constructor leaves the ordinary “Continue to checkout” link in place, which navigates to the processor’s hosted page.

const methodData = [{
supportedMethods: "https://google.com/pay",
data: { environment: "PRODUCTION", apiVersion: 2, apiVersionMinor: 0 /* merchant config */ },
}];
const details = {
total: { label: "Total", amount: { currency: "USD", value: "29.99" } },
};
async function prepareCheckout() {
const hosted = document.querySelector("#hosted-checkout");
const wallet = document.querySelector("#wallet-button");
if (!("PaymentRequest" in window)) {
hosted.hidden = false;
return;
}
const request = new PaymentRequest(methodData, details);
let available = false;
try {
available = await request.canMakePayment();
} catch (err) {
console.warn(`${err.name}: ${err.message}`); // rejected when the browser's query quota is exceeded
}
wallet.hidden = !available;
hosted.hidden = available;
}
prepareCheckout();

Keep the request object you tested with: canMakePayment() does not consume it, so the click handler in the next example can call show() on the same instance.

Capturing on the server, then closing the sheet with complete()

Section titled “Capturing on the server, then closing the sheet with complete()”

show() must run inside the click handler so transient activation is present. Send response.toJSON() to the server, and call complete("success") or complete("fail") as soon as the server answers; the sheet stays open until you do, and Chrome closes it on its own after a timeout as if complete() had been called with no argument.

const wallet = document.querySelector("#wallet-button");
wallet.addEventListener("click", async () => {
const request = new PaymentRequest(methodData, details);
let response;
try {
response = await request.show();
} catch (err) {
if (err.name === "AbortError") return; // user closed the sheet
location.assign("/checkout/hosted"); // any other rejection: fall back to the hosted page
return;
}
const result = await fetch("/api/charge", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(response.toJSON()),
});
await response.complete(result.ok ? "success" : "fail");
if (result.ok) location.assign("/checkout/thanks");
});

complete("fail") shows the browser’s failure state and closes the sheet; the page is then responsible for telling the user what to do next.

Repricing when the shipping address changes

Section titled “Repricing when the shipping address changes”

With requestShipping: true the sheet lets the user pick an address and fires shippingaddresschange on the request. Call event.updateWith() synchronously inside the listener with a promise for the new details; the sheet shows a spinner until it settles. The specification lets the browser redact parts of the address until the user authorises, so compute shipping from country, region, and postalCode rather than the street lines.

const request = new PaymentRequest(methodData, {
total: { label: "Total", amount: { currency: "USD", value: "29.99" } },
shippingOptions: [
{ id: "ground", label: "Ground (5 days)", amount: { currency: "USD", value: "0.00" }, selected: true },
],
}, { requestShipping: true });
request.addEventListener("shippingaddresschange", (event) => {
const { country, postalCode } = event.target.shippingAddress;
event.updateWith(
fetch(`/api/shipping?country=${country}&postal=${postalCode}`)
.then((r) => r.json())
.then((quote) => ({
total: { label: "Total", amount: { currency: "USD", value: quote.total } },
shippingOptions: quote.options,
}))
);
});

Returning shippingOptions: [] from the quote tells the sheet that nothing can be shipped to that address; add error: "We do not ship to this region" to the update so the user sees why.

Specifications

SpecificationStatus
Payment Request APIW3C
Payment Request API: constructorW3C
Payment Request API: show() methodW3C
Payment Request API: canMakePayment() methodW3C
  • Legend
  • Yes
  • Partial
  • Flag
  • No
  • Unknown
Browser / PlatformSupportVersionsConfidenceSourceNotes
Chrome (Android)Yes61mediumsource—
Chrome (Desktop)Yes61mediumsource—
Edge (Desktop)Yes79mediumsource—
Safari (iOS)Yes11.1mediumsource1
Safari (macOS)Yes11.1mediumsource2
Firefox (Desktop)No—mediumsource3
Samsung InternetYes7.0mediumsource—
  1. Backed by Apple Pay as the payment method.
  2. Backed by Apple Pay.
  3. Implementation shipped then disabled; not available by default.

Ecosystem & commercial policy

EntityTypeContextStatusSponsoredNotes
Apple Paypayment_sdkSafari / iOSYesNoWorks in Safari via Payment Request; merchant-domain verification required.
Stripepayment_sdkCross-browserYesNoStripe wraps Payment Request as the Payment Request Button / Payment Element.
Google Play billingstore_policyGoogle Play TWANoNoTWAs distributing digital goods must use Play Billing, not Payment Request, per Play policy.

Source data: /compatibility/payment-request.json · Global usage: 90 % (StatCounter 2026-05)

Source: spec · MDN · Last verified 2026-06-24 · Confidence: medium (computed from sources)