Introduction to the Web Payment API
The Payment Request API is a W3C standard that provides a unified, browser-native interface for collecting payment and shipping information from users. Instead of building custom checkout forms with dozens of input fields, developers can leverage a consistent, secure UI that browsers handle natively. This reduces friction, increases conversion rates, and offloads sensitive data handling to the browser.
This guide walks through everything you need to know to integrate the Payment Request API into a modern web application, from basic usage to advanced flows and best practices.
What Is the Payment Request API?
The Payment Request API is a JavaScript interface exposed by the browser that opens a native dialog for collecting payment credentials, shipping addresses, and contact information. It is designed to be:
- Consistent — the same UI patterns across merchants.
- Secure — payment instruments are stored and transmitted by the browser, not the merchant page.
- Extensible — supports multiple payment methods including basic card, Google Pay, Apple Pay, and other third-party payment apps.
- Fast — autofills stored credentials, reducing checkout time dramatically.
The API consists of three primary objects: PaymentRequest, PaymentAddress, and PaymentResponse. The lifecycle is straightforward: construct a request, show it to the user, await their response, and process the result.
Why It Matters
Traditional checkout forms suffer from high abandonment rates. Users must type card numbers, expiration dates, CVVs, billing addresses, and contact details — often on mobile keyboards. The Payment Request API addresses these pain points by:
- Eliminating redundant form fields.
- Reusing previously saved payment instruments.
- Providing a familiar, accessible UI.
- Reducing PCI scope for merchants, since raw card data can be tokenized by the browser or payment app.
- Supporting platform-specific wallets like Apple Pay and Google Pay through a single integration point.
For e-commerce sites, even a small reduction in checkout friction translates to measurable revenue gains. The Payment Request API is one of the highest-impact features available to web developers today.
Browser Support and Feature Detection
Before using the API, always detect support. While most modern browsers support the core interface, the available payment methods vary by platform.
if (window.PaymentRequest) {
console.log('Payment Request API is supported');
} else {
console.log('Falling back to traditional checkout form');
}
Feature detection should also check whether a specific payment method is available before showing the button:
async function canMakePayment(methods) {
if (!window.PaymentRequest) return false;
try {
const request = new PaymentRequest(methods, { total: { label: 'Test', amount: { currency: 'USD', value: '0.00' } } });
return await request.canMakePayment();
} catch (err) {
return false;
}
}
Basic Usage
Constructing a PaymentRequest
A PaymentRequest requires two arguments: an array of supported payment methods and payment details. A third optional argument specifies additional options such as requesting shipping or contact information.
const supportedPaymentMethods = [
{
supportedMethods: 'https://google.com/pay',
data: {
merchantIdentifier: 'BCR2DN4TR5WJZKZE',
environment: 'TEST',
apiVersion: 2,
apiVersionMinor: 0
}
},
{
supportedMethods: 'basic-card',
data: {
supportedNetworks: ['visa', 'mastercard', 'amex', 'discover'],
supportedTypes: ['credit', 'debit']
}
}
];
const paymentDetails = {
displayItems: [
{ label: 'Subtotal', amount: { currency: 'USD', value: '75.00' } },
{ label: 'Shipping', amount: { currency: 'USD', value: '5.00' } },
{ label: 'Tax', amount: { currency: 'USD', value: '6.40' } }
],
total: {
label: 'Total due',
amount: { currency: 'USD', value: '86.40' }
}
};
const options = {
requestPayerName: true,
requestPayerEmail: true,
requestPayerPhone: true,
requestShipping: true,
shippingType: 'shipping'
};
const request = new PaymentRequest(
supportedPaymentMethods,
paymentDetails,
options
);
Showing the Payment Sheet
Call show() to display the native payment dialog. This method returns a promise that resolves with a PaymentResponse when the user confirms, or rejects if they dismiss the dialog.
async function checkout() {
try {
const response = await request.show();
// Send payment data to your server for processing
const result = await processPaymentOnServer(response);
if (result.success) {
response.complete('success');
showOrderConfirmation(result.orderId);
} else {
response.complete('fail');
showErrorMessage(result.error);
}
} catch (err) {
if (err.name === 'AbortError') {
console.log('User closed the payment sheet');
} else {
console.error('Payment failed:', err);
}
}
}
The complete() method tells the browser whether the transaction succeeded or failed, allowing it to dismiss the sheet with appropriate visual feedback.
Handling the PaymentResponse
The PaymentResponse object contains all the information the user provided. Its structure depends on the selected payment method.
async function processPaymentOnServer(response) {
const payload = {
methodName: response.methodName,
details: response.details,
payerName: response.payerName,
payerEmail: response.payerEmail,
payerPhone: response.payerPhone,
shippingAddress: response.shippingAddress,
shippingOption: response.shippingOption
};
const res = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
return res.json();
}
For basic-card, response.details contains the cardholder name, card number, expiry month and year, and CVV. For wallet-based methods like Google Pay, it contains a tokenized payment string that you forward to your payment processor.
Shipping Options and Dynamic Updates
When requestShipping is enabled, the API emits events as the user changes their shipping address or selected shipping option. You can listen to these events and update the total dynamically.
request.addEventListener('shippingaddresschange', async (event) => {
event.updateWith(updateShippingDetails(request.shippingAddress));
});
request.addEventListener('shippingoptionchange', async (event) => {
event.updateWith(updateTotalForShippingOption(request.shippingOption));
});
function updateShippingDetails(address) {
const shippingCost = calculateShipping(address.country, address.postalCode);
return {
displayItems: [
{ label: 'Subtotal', amount: { currency: 'USD', value: '75.00' } },
{ label: 'Shipping', amount: { currency: 'USD', value: shippingCost } },
{ label: 'Tax', amount: { currency: 'USD', value: '6.40' } }
],
total: {
label: 'Total due',
amount: { currency: 'USD', value: (75 + shippingCost + 6.4).toFixed(2) }
},
shippingOptions: getShippingOptionsForAddress(address)
};
}
The updateWith() method accepts either a PaymentDetailsUpdate object or a promise that resolves to one. This lets you fetch shipping rates asynchronously from your server.
Integrating with Payment Processors
In production, you rarely handle raw card data yourself. Instead, you tokenize it through a processor like Stripe, Braintree, or Adyen. The Payment Request API integrates cleanly with these services.
async function processWithStripe(response) {
if (response.methodName === 'basic-card') {
const { cardNumber, cardSecurityCode, expiryMonth, expiryYear } = response.details;
const tokenResponse = await fetch('https://api.stripe.com/v1/tokens', {
method: 'POST',
headers: {
'Authorization': 'Bearer pk_test_your_publishable_key',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
'card[number]': cardNumber,
'card[exp_month]': expiryMonth,
'card[exp_year]': expiryYear,
'card[cvc]': cardSecurityCode
})
});
const token = await tokenResponse.json();
return chargeTokenOnServer(token.id);
} else if (response.methodName === 'https://google.com/pay') {
return chargeGooglePayTokenOnServer(response.details.paymentMethodData.tokenizationData.token);
}
}
By tokenizing on the client and charging on the server, you keep sensitive data out of your backend and minimize PCI compliance scope.
Best Practices
Always Provide a Fallback
Not every browser or user has a configured payment instrument. Always offer a traditional checkout form as a fallback path.
async function initiateCheckout() {
if (!window.PaymentRequest) {
return showTraditionalForm();
}
const canPay = await canMakePayment(supportedPaymentMethods);
if (!canPay) {
return showTraditionalForm();
}
return checkout();
}
Validate on the Server
Never trust client-side data. The Payment Request API improves UX, but it does not replace server-side validation. Always re-validate the cart, prices, shipping options, and payment tokens on your backend before fulfilling the order.
Use Idempotency Keys
Network failures can cause duplicate charges. Generate a unique idempotency key per checkout attempt and send it with your charge request so your processor can safely retry without double-charging.
Handle Aborts Gracefully
Users frequently dismiss the payment sheet. Treat AbortError as a normal user action, not an error condition. Avoid showing alarming error messages for this case.
Keep the Total Accurate
The total in your PaymentDetails must always reflect the final amount the user will be charged. If shipping or tax changes, update the total via updateWith() before the user confirms.
Test Thoroughly
Test across browsers and devices. Chrome, Edge, Safari, and Firefox have different levels of support for payment methods. Use test cards from your processor and verify the full flow including shipping address changes, option changes, and edge cases like unsupported regions.
Security Considerations
The Payment Request API is designed with security in mind, but you still have responsibilities:
- Serve your site over HTTPS — the API is only available in secure contexts.
- Never log full card numbers or CVVs from
response.details. - Prefer token-based payment methods over
basic-cardwhen possible. - Validate the origin and integrity of payment callbacks on your server.
- Comply with PCI DSS requirements relevant to your integration type.
Conclusion
The Payment Request API offers a powerful, standardized way to streamline checkout on the web. By delegating form rendering and credential storage to the browser, you reduce friction, improve security, and boost conversion rates. Start with feature detection and a solid fallback, integrate with your payment processor for tokenization, handle shipping and option changes dynamically, and always validate on the server. With these patterns in place, you can deliver a fast, reliable checkout experience that works across modern browsers and devices while keeping sensitive data out of your infrastructure.