jsguides

The Payment Request API: Browser-Native Checkout Made Simple

Building a payment checkout form from scratch is tedious and error-prone. The Payment Request API gives you a standardized browser-native interface for collecting payment details, so you stop wrestling with credit card fields and let the browser handle the heavy lifting.

The API centers on a single object: PaymentRequest. You construct it with the payment methods you accept and the purchase details, then call .show() to surface the browser’s payment sheet.

Creating a payment request

The PaymentRequest constructor takes three arguments:

const request = new PaymentRequest(
  methodData,   // supported payment methods
  details,      // purchase amount and line items
  options       // optional: request name, email, shipping, etc.
);

methodData is an array describing what payment methods you accept. Each entry specifies a payment method identifier and any data the method requires. For basic card payments, you declare supported networks and card types that the browser will solicit from the user:

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

The basic-card method works across all supporting browsers without any extra setup. For more control, you can reference a custom payment handler URL instead, which lets you plug into your own payment infrastructure and accept alternative payment methods beyond cards:

const methodData = [
  { supportedMethods: "https://example.com/pay" }
];

With the payment method defined, next comes details: the purchase breakdown the browser displays in the payment sheet. This includes line items, currency, and the total the user confirms before authorizing the transaction. Each item is structured as follows:

const details = {
  id: "order-abc-123",
  displayItems: [
    {
      label: "Widget Pro",
      amount: { currency: "USD", value: "29.99" }
    },
    {
      label: "Shipping",
      amount: { currency: "USD", value: "5.00" }
    }
  ],
  total: {
    label: "Total",
    amount: { currency: "USD", value: "34.99" }
  }
};

The total field is what the user sees and confirms before paying. displayItems are optional but provide a line-item breakdown that helps users understand what they are paying for. The options argument controls what additional information you collect from the payer:

const options = {
  requestPayerName: true,
  requestPayerEmail: true,
  requestShipping: true,
  shippingType: "shipping"
};

Showing the payment sheet

With the constructor arguments in place, call .show() to surface the browser’s native payment UI. This returns a promise that resolves when the user approves the transaction, giving you the payment response with the method used and payment details:

request.show().then((paymentResponse) => {
  // User accepted — process the payment
  const { methodName, details } = paymentResponse;

  // Send details to your payment processor
  processPayment(methodName, details).then(() => {
    // Tell the browser the payment is done
    paymentResponse.complete("success");
  });
}).catch((err) => {
  if (err.name === "NotSupportedError") {
    // Browser doesn't support this payment method — fall back
    window.location.href = "/legacy-checkout";
  }
  // Handle cancellation or other errors
});

.show() returns a promise. If the user accepts the payment, it resolves with a PaymentResponse object containing methodName (the payment method used) and details (method-specific response data from the payment handler).

Call .complete() when you are done; this dismisses the browser UI and tells it the payment succeeded or failed.

One critical constraint: you can only call .show() once per PaymentRequest instance. After the sheet closes (for any reason), you must create a fresh PaymentRequest for the next purchase.

Checking payment availability

Before showing a payment sheet on a live site, check whether the user’s browser can actually make a payment. .canMakePayment() returns a promise that resolves to a boolean:

const request = new PaymentRequest(methodData, {
  total: { label: "Stub", amount: { currency: "USD", value: "0.01" } }
});

request.canMakePayment().then((canPay) => {
  if (canPay) {
    checkoutButton.textContent = "Pay now";
  } else {
    checkoutButton.textContent = "Set up payment";
  }
});

Use this to customize your checkout button: “Pay now” when the user already has valid payment credentials, “Set up payment” when they need to add a card first. You can also use it as a gate: only use PaymentRequest if .canMakePayment() returns true, otherwise redirect to a legacy form.

Note that .canMakePayment() requires the same methodData structure between calls if you call it multiple times. For checking before you know all the prices, use a stub amount in details; the method data is what matters for the check.

Aborting a payment

If you need to cancel an in-progress payment request:

request.abort().then(() => {
  // Payment sheet closed
}).catch((err) => {
  // Abort failed — sheet already closed or already complete
});

After aborting an in-progress payment, you need to know when the sheet closes. Then, before showing the payment UI, always check that the user’s browser supports the API to avoid runtime errors on unsupported or insecure platforms in production:

if (!window.PaymentRequest) {
  // Redirect to legacy checkout
  window.location.href = "/checkout/legacy";
}

The API has good cross-browser support (Chrome, Edge, Safari, Firefox), but it only works in secure contexts (HTTPS). It also does not work in iframes unless the iframe is same-origin and has the appropriate permissions policy.

Fallback checkout strategy

The Payment Request API should sit inside a larger checkout plan, not replace it entirely. If the browser cannot use the API, the app still needs a clear route to a traditional form. That fallback should preserve the same order details, the same totals, and the same confirmation language so the user does not feel like they are switching to a different product halfway through.

This is also a good place to keep the checkout flow honest. A simple fallback makes it easier to test what happens when the API is unavailable. That helps you design a checkout that works in more than one environment instead of depending on a single browser path.

Shipping and address details

When you request shipping information, think about how the rest of the app will use it. The shipping address is not just a form field. It usually affects tax, delivery options, and the final amount the user sees. If your totals can change, the code that updates them should be clear and quick to follow.

It helps to keep the shipping logic separate from the payment submission itself. That way the payment response stays focused on the browser interaction, while your business rules decide which address rules, shipping methods, or validation checks apply. Clear boundaries make the flow easier to debug.

Testing across browsers

Browser support is broad, but each browser can still handle the checkout sheet a little differently. That makes real testing important. Check the behavior on desktop and mobile, and verify that the fallback path works when the API is missing or disabled. A checkout feature is only as good as its least cooperative browser.

It is also smart to test the full flow with a realistic order. The API may be fine in isolation, but a real cart can expose issues with totals, shipping updates, or confirmation handling. A few end-to-end checks are worth a lot here because payment flows are sensitive and users notice small rough edges quickly.

Real checkout timing

Checkout code has a lot of timing pressure. The cart can change, shipping can update, and the browser payment sheet can appear or disappear very quickly. That means the code around the API should be written so each step is easy to pause and inspect. Short functions and clear state transitions help a lot here.

The goal is to keep the user informed without making them repeat work. If the checkout changes after the sheet opens, the app should explain why and keep the path forward obvious. A calm, predictable flow is better than a clever one when money is involved.

Limitations

The Payment Request API handles the collection of payment information; it does not process the payment itself. After you get the PaymentResponse.details, you still need to send that data to your payment processor (Stripe, Square, PayPal, etc.) and handle the result.

Some browsers may not support all payment method identifiers. Always catch NotSupportedError and provide a fallback.

See Also