← Back to DevBytes

Web Payment API: Complete Guide

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:

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:

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:

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles