← Back to DevBytes

Implementing Web Payment API in Modern Web Applications

Introduction to the Web Payment API

The Web Payment API is a set of browser-native interfaces that enable a seamless, secure, and user-friendly checkout experience directly within web applications. It consists primarily of two specifications: the Payment Request API and the Payment Handler API. Together, they allow merchants to request payments from users, while browsers mediate the transaction, storing and presenting payment methods (like credit cards, Google Pay, Apple Pay) and handling the secure exchange of payment credentials.

Instead of forcing users to re-enter card details or navigate through multiple redirect pages, the Web Payment API provides a standardized browser UI that collects payment information, verifies it, and returns a payment response that the merchant can process. This reduces cart abandonment, increases conversion rates, and improves accessibility.

Why the Payment Request API Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Modern web applications demand fast, frictionless checkout. The traditional approach—HTML forms collecting card numbers, expiry dates, CVV codes—suffers from several problems:

The Payment Request API addresses these issues by delegating the collection of payment details to the browser. The browser can draw on locally stored payment instruments (tokenized cards, bank accounts, digital wallets) and present a consistent, trusted UI. This means:

Core Concepts

Before diving into code, it’s helpful to understand the key building blocks:

Implementing a Basic Payment Request

Let’s walk through a minimal checkout flow using the Payment Request API. We’ll assume a simple e‑commerce scenario: a shopping cart with a total of 29.99 EUR.

Step 1: Feature Detection

Always check that the API is available. This helps provide a graceful fallback.


if (window.PaymentRequest) {
  // Proceed with the Payment Request flow
} else {
  // Fallback to a traditional checkout form
  showTraditionalCheckout();
}

Step 2: Define Supported Payment Methods

The most straightforward method is 'basic-card', which lets the browser collect any credit, debit, or prepaid card details. You can optionally specify supported networks (e.g., visa, mastercard).


const methodData = [
  {
    supportedMethods: 'basic-card',
    data: {
      supportedNetworks: ['visa', 'mastercard', 'amex'],
      supportedTypes: ['credit', 'debit']
    }
  }
];

Step 3: Define Payment Details (Transaction Total)

Create a details object that includes the total. Currency must use the ISO 4217 code (e.g., 'EUR', 'USD'). You can also add display items for transparency.


const details = {
  total: {
    label: 'Total',
    amount: { currency: 'EUR', value: '29.99' }
  },
  displayItems: [
    {
      label: 'E‑Book: Modern Web Payments',
      amount: { currency: 'EUR', value: '29.99' }
    }
  ]
};

Step 4: Create and Show the PaymentRequest

Instantiate PaymentRequest and call show(). This method returns a promise that resolves with a PaymentResponse when the user completes the interaction, or rejects if they cancel.


const request = new PaymentRequest(methodData, details);

request.show()
  .then(response => {
    // Process the payment response
    handlePaymentResponse(response);
  })
  .catch(error => {
    if (error.code === DOMException.ABORT_ERR) {
      console.log('User cancelled the payment flow');
    } else {
      console.error('Payment error:', error);
    }
  });

At this point, the browser presents its payment sheet. The user selects a stored card (or enters a new one), reviews the total, and authorizes the payment. Once done, the promise resolves.

Handling Payment Response and Validation

The PaymentResponse contains a details property with method‑specific data (for 'basic-card', it includes cardholder name, card number, expiry, and CVV in clear text—but only within the browser’s trusted scope). However, directly handling raw card data is insecure and non‑compliant with PCI‑DSS in production. Instead, you should tokenize the card details on your server or use a payment service provider.

Extracting Basic Card Response

For demonstration purposes (and only in a controlled environment), here’s how you could read the response. In a real application, you must never expose raw card data to your backend without proper tokenization.


function handlePaymentResponse(response) {
  // For basic-card, details contains cardholderName, cardNumber, etc.
  const cardholderName = response.details.cardholderName;
  const cardNumber     = response.details.cardNumber;
  const expiryMonth    = response.details.expiryMonth;
  const expiryYear     = response.details.expiryYear;
  const cardSecurityCode = response.details.cardSecurityCode;

  // Construct a tokenization request to your PSP
  const tokenRequest = {
    cardholderName,
    cardNumber,
    expiryMonth,
    expiryYear,
    cardSecurityCode,
    // Additional PSP‑specific fields
  };

  // Send tokenRequest to your backend (never store raw card data client-side)
  fetch('/api/tokenize-card', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(tokenRequest)
  })
  .then(res => res.json())
  .then(tokenResponse => {
    // Tokenization succeeded, now complete the transaction
    completeTransaction(tokenResponse.token);
    response.complete('success');
  })
  .catch(err => {
    response.complete('fail');
    console.error('Tokenization failed:', err);
  });
}

Completing the Payment

Always call response.complete() with a status ('success', 'fail', or 'unknown'). This tells the browser to close the payment sheet and optionally display a success or error state. Failing to call complete() leaves the UI hanging.


function completeTransaction(token) {
  // Send the token to your backend to capture the charge
  fetch('/api/process-payment', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token, amount: '29.99', currency: 'EUR' })
  })
  .then(res => res.json())
  .then(result => {
    if (result.success) {
      // Update UI: show order confirmation
      displayOrderConfirmation();
    } else {
      // Inform the user of failure
      displayPaymentError(result.error);
    }
  });
}

Supporting Multiple Payment Methods

A major advantage of the Payment Request API is the ability to offer several payment methods simultaneously, without extra UI work. The browser automatically presents the user with available options (e.g., cards on file, Google Pay, Apple Pay).

Adding Google Pay and Apple Pay

To integrate Google Pay, you include a method identifier like 'https://google.com/pay' and provide configuration (merchant ID, allowed card networks). Apple Pay uses the identifier 'https://apple.com/apple-pay' with similar configuration. Both require you to register merchant identifiers with the respective platforms.


const methodData = [
  {
    supportedMethods: 'basic-card',
    data: {
      supportedNetworks: ['visa', 'mastercard'],
      supportedTypes: ['credit', 'debit']
    }
  },
  {
    supportedMethods: 'https://google.com/pay',
    data: {
      environment: 'TEST',          // Use 'PRODUCTION' when live
      apiVersion: 2,
      apiVersionMinor: 0,
      allowedPaymentMethods: ['CARD', 'TOKENIZED_CARD'],
      merchantInfo: {
        merchantId: '0123456789',   // Your Google Pay merchant ID
        merchantName: 'Example Shop'
      },
      // Transaction info must match details.total
      transactionInfo: {
        totalPriceStatus: 'FINAL',
        totalPrice: '29.99',
        currencyCode: 'EUR',
        countryCode: 'DE'
      }
    }
  },
  {
    supportedMethods: 'https://apple.com/apple-pay',
    data: {
      version: 3,                   // Apple Pay version
      merchantIdentifier: 'merchant.com.example',
      supportedNetworks: ['visa', 'mastercard'],
      merchantCapabilities: ['supports3DS', 'supportsCredit', 'supportsDebit'],
      countryCode: 'DE',
      currencyCode: 'EUR'
    }
  }
];

When show() is called, the browser evaluates which methods the user has configured. The user can choose Google Pay if their device supports it, or fall back to a stored card. You handle the response accordingly—Google Pay returns a payment token that you verify with Google’s servers, while Apple Pay returns an encrypted payment token.

Handling Different Response Types

After show(), examine response.methodName to determine how to process the response. For tokenized methods, you’ll typically send the token to your backend for decryption and capture.


function handlePaymentResponse(response) {
  const method = response.methodName;

  if (method === 'basic-card') {
    // Handle basic card tokenization (as shown earlier)
    tokenizeBasicCard(response);
  } else if (method === 'https://google.com/pay') {
    // Google Pay returns a JSON string in response.details
    const tokenData = JSON.parse(response.details.token);
    processGooglePayToken(tokenData);
  } else if (method === 'https://apple.com/apple-pay') {
    // Apple Pay returns an encrypted payment token
    const applePayToken = response.details.token;
    processApplePayToken(applePayToken);
  }

  // Remember to complete() after processing
}

Creating a Payment Handler (Service Worker)

If your web application wants to act as a payment app itself (e.g., a banking app that manages its own digital currency or a proprietary wallet), you can register a Payment Handler via a service worker. This allows your app to handle payment requests from other websites when a user selects your payment method.

Registering a Payment Handler

First, register a service worker and then use PaymentManager to declare the supported payment method identifier.


// In your main page script (e.g., wallet-app.js)
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/payment-handler-sw.js')
    .then(registration => {
      // Check if PaymentManager is available
      if (registration.paymentManager) {
        registration.paymentManager.paymentMethodManagers
          .set('https://example.com/custom-pay', {
            enabled: true,
            // Optional: human-readable label
            label: 'Example Wallet'
          });
      }
    });
}

Service Worker Implementation

The service worker listens for the paymentrequest event, opens a custom payment window, and responds with a payment response object. Here’s a minimal example:


// payment-handler-sw.js
self.addEventListener('paymentrequest', event => {
  const request = event.paymentRequest;
  const details = request.details;   // PaymentDetails from the merchant
  const methodName = request.methodName;

  // Open a custom UI (could be a new window, a popup, or a built-in form)
  event.respondWith(new Promise((resolve, reject) => {
    // Simulate user authentication and payment authorization
    // In a real wallet, you'd show a UI and wait for user action
    const paymentResponse = {
      methodName: methodName,
      details: {
        token: 'secure-wallet-token-abc123',
        walletId: 'user-wallet-id'
      }
    };

    // Resolve with the payment response
    resolve(paymentResponse);
  }));
});

Now any merchant page that includes 'https://example.com/custom-pay' in its methodData will see your wallet as an available payment option. The user can select it, and the service worker will handle the transaction.

Best Practices

Browser Support and Feature Detection

The Payment Request API is supported in most modern browsers, including Chrome, Edge, Samsung Internet, and Opera. Safari supports it on macOS and iOS (with Apple Pay integration). Firefox has limited support for basic card but not yet for digital wallets. Always perform runtime checks:


if (window.PaymentRequest) {
  try {
    const request = new PaymentRequest(methodData, details);
    request.canMakePayment()
      .then(canPay => {
        if (canPay) {
          // Show the "Pay with Browser" button
          enablePaymentButton();
        } else {
          // User has no active payment method; fall back
          showFallbackCheckout();
        }
      });
  } catch (e) {
    // Fallback on error
    showFallbackCheckout();
  }
} else {
  showFallbackCheckout();
}

The canMakePayment() method (and its modern equivalent hasEnrolledInstrument()) lets you determine whether to display the native payment option at all, improving UX by hiding a button that would fail.

Conclusion

The Web Payment API transforms checkout from a tedious, error‑prone form‑filling process into a streamlined, browser‑mediated interaction. By leveraging the Payment Request API, you give users a fast, secure way to pay with stored cards and digital wallets, while your application remains PCI‑compliant because raw card data stays out of your JavaScript context. Adding support for multiple payment methods, including Google Pay and Apple Pay, becomes straightforward. For organizations building custom payment methods, the Payment Handler API allows a web app to serve as a fully‑fledged payment app through service workers.

Adopting these standards not only improves conversion rates but also future‑proofs your checkout experience. As browsers continue to evolve and integrate deeper with device‑native payment capabilities, the Web Payment API will remain the cornerstone of modern web commerce. Start by implementing a basic card flow, then gradually expand to digital wallets, and always combine with robust server‑side validation and tokenization to keep transactions safe and seamless.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles