Imagine trying to buy something online, maybe on your favorite ecommerce store. After surfing and searching, you finally find the perfect item. You add it to your cart and try to check out, but suddenly, you’re hit with a long and boring form requesting your billing address, contact information and, most importantly, card details. Without the convenience of auto-fills that we have come to expect, such an experience can be frustrating and clunky.
Now, imagine if your browser simplifies this process by securely pulling the payment details you saved, eliminating the need to fill out complex forms. That is exactly what the Payment Request API does.
It was designed with the user experience in mind, simplifying and speeding up the checkout process while providing a more secure, consistent and seamless payment experience across websites.
In this article, we’ll explain the Payment Request API, its benefits and how it can be used in modern applications.
You only need to have a basic understanding of JavaScript.
When learning about the Payment Request API, the definition that stood out for me was from Zach Koch’s Google I/O 2017 talk on “The Future of Web Payments.” He described the API as “an open, built-into-the-web, designed-to-be-fast, ready-to-be-used-today API for transacting on the web.”
Now, let’s break this down:
This enhances the checkout experience, making payments faster and less frustrating for users. Unlike some traditional approaches, it provides a more streamlined flow, allowing users to complete the payment process without leaving the checkout page.
The Payment Request API enables user agents such as the browser to act as intermediaries between the user, merchant and payment method in a transaction.
Before continuing the rest of the article, let’s clarify some misconceptions about the Payment Request API.
First and most importantly, the Payment Request API does not process payments. It collects payment details and then hands them over to the merchant’s integrated payment process, which could be Stripe, PayPal, etc.
Next, the Payment Request API does not validate payment information. When integrated, the browser prefills the user’s payment details (if stored) but does not validate those details. Actual verification is done via the merchant’s payment provider.
It is also not a security risk. The API does not expose sensitive data to websites. Instead, it handles the data securely, and merchants receive only what is necessary.
Lastly, the Payment Request API does not entirely replace payment forms. It eliminates the need to fill out the form manually. However, a fallback form-based checkout might be used when the user’s browser does not support the API (we will look at the API’s browser compatibility in a later section).
In addition to the benefit of a faster checkout process, consistent user experience, etc., explained so far in this article, this section highlights a few more benefits that the Payment Request API offers.
Browser compatibility is an important factor to consider when using Web APIs. According to https://caniuse.com/payment-request, the Payment Request API is supported by recent versions of most modern browsers, such as Google Chrome, Edge, etc.
However, we can always run the window.PaymentRequest
during implementation to check compatibility before using the API.
Here is a sample code snippet showing what this check might look like:
const handleCheckout = () => {
if (window.PaymentRequest) {
//proceed to checkout, trigger the PaymentRequest API
checkout();
} else {
//case of incompatibility
console.log("You can't use this feature!");
}
};
Now that we have learned about the Web Payment API, let’s see how to use it.
The Payment Request API exposes a PaymentRequest
interface, which serves as the primary entry point to the API. To use the API, a new PaymentRequest
instance must be created, and the necessary parameters must also be passed to its constructor.
Here is the basic syntax for instantiating PaymentRequest
:
const paymentRequest = new PaymentRequest(methodData, details, options);
The PaymentRequest
constructor takes two required parameters (the payment methodData
and details
) and an optional options
parameter.
Let’s take a look at each of these parameters individually.
The first required parameter to instantiate the PaymentRequest
is an array of objects that defines the various supported payment methods. The methodData
array holds information about the various enabled payment providers.
Individual objects in the payment methodData
array relating to a specific payment provider must define two elements: a required supported methods
identifier and an optional data
object.
The supportedMethods
can be for:
On the other hand, the optional data
object element can be used to define optional parameters allowed by the payment method provider. Note that the content of this data element is based on what gets defined in supportedMethods
.
Here is a snippet that shows what the payment method data array would look like:
const methodData = [
{
supportedMethods: "basic-card",
data: {
supportedNetworks: ["visa", "mastercard"],
},
},
{
supportedMethods: "https://google.com/pay",
},
];
This snippet shows a basic definition that supports both basic card and Google Pay as payment options.
The second required parameter to pass when instantiating the PaymentRequest
is an object that defines the details of a given transaction.
It consists of a total
object that specifies the total amount to be paid by the user and an optional displayItems
object that defines the item breakdown, including other fees such as shipping cost, tax, etc.
Here is a sample snippet:
const details = {
total: {
label: "Total Amount",
amount: { currency: "USD", value: "25.00" },
},
displayItems: [
{
label: "Product 1",
amount: { currency: "USD", value: "15.00" },
},
{
label: "Product 2",
amount: { currency: "USD", value: "10.00" },
},
],
};
Here, we can see the total amount the user will pay and a list of the items contributing to the total cost.
The third parameter for instantiating the PaymentRequest
is the options
object. Unlike the others, it is optional. It allows merchants to specify additional preferences, such as required shipping details, contact information and payment capabilities.
Here is a list of some of the supported options:
const options = {
requestPayerName: true,
requestPayerEmail: true,
requestPayerPhone: true,
requestShipping: true,
requestBillingAddress: true,
};
From the property names, we can see that the various options can be used to request extra information from users, such as email, name, shipping information and so on.
Putting all of this together, this is what a complete PaymentRequest
instantiation would look like:
//payment methods definition
const methodData = [
{
supportedMethods: "basic-card",
data: {
supportedNetworks: ["visa", "mastercard"],
},
},
{
supportedMethods: "https://google.com/pay",
},
];
//details definition
const details = {
total: {
label: "Total Amount",
amount: { currency: "USD", value: "25.00" },
},
displayItems: [
{
label: "Product 1",
amount: { currency: "USD", value: "15.00" },
},
{
label: "Product 2",
amount: { currency: "USD", value: "10.00" },
},
],
};
//options definition
const options = {
requestPayerName: true,
requestPayerEmail: true,
requestPayerPhone: true,
requestShipping: true,
requestBillingAddress: true,
};
//instantiation
const paymentRequest = new PaymentRequest(methodData, details, options);
Here, we can see the complete payment methods, details, options definition and interface instantiation.
The PaymentRequest
instance exposes several methods and properties that help manage the payment flow. These include checking payment availability, showing the interface and handling user interactions.
Once the PaymentRequest
instantiation is complete, we can call the canMakePayment
method on the created instance to verify whether the user can proceed with the payment based on the construction of the payment object. It checks whether the user has at least one valid payment method available, which helps prevent issues if no supported payment methods exist.
Here is a snippet that shows a sample usage of the canMakePayment
method:
//payment methods definition
const methodData = [
//....
];
//details definition
const details = {
//...
};
//options definition
const options = {
//...
};
//instantiation
const paymentRequest = new PaymentRequest(methodData, details, options);
//check if the user can make a payment
async function checkPaymentAvailability() {
try {
const canPay = await paymentRequest.canMakePayment();
if (canPay) {
console.log("User can make a payment.");
} else {
console.log("No supported payment methods available.");
}
} catch (error) {
console.error("Error checking payment availability:", error);
}
}
checkPaymentAvailability();
If the definition is good, canMakePayment()
resolves to true; otherwise, it returns false. This provides a better user experience and helps prevent unnecessary error prompts.
The next step is to display the payment interface, and we can do this by calling the show
method on the PaymentRequest
instance.
The show
method triggers the browser’s native payment interface. It returns a promise that resolves with a PaymentResponse
object once the user completes or cancels the payment.
Here is a sample snippet that shows how it works:
async function displayPaymentUI() {
try {
//calls show() on the `PaymentRequest` instance
const paymentResponse = await paymentRequest.show();
// payment successful
await paymentResponse.complete("success");
} catch (error) {
console.error("Payment was canceled or failed:", error);
}
}
displayPaymentUI();
In the code above, request.show
displays the payment interface. On success, a PaymentResponse
object is returned. Otherwise, if something goes wrong or the user cancels the payment process, an error is returned which can be handled appropriately.
In addition to the canMakePayment
and show
methods that we have looked at in this section, the Payment Request API provides other valuable properties, methods and events on its instance. Some of them are abort,
which can be used to cancel a pending request, and paymentMethodChange,
which gets triggered whenever the user changes payment instruments, like switching from a credit card to a debit card.
You can review the complete list of properties, methods and events here.
Now that we’ve learned how to use the Payment Request API, we’re ready to see it in action. In this section, we will create a Next.js application for the demo.
Open your terminal and run the following command to create a new Next.js 14 project:
npx create-next-app@14
Enter a name for the demo project and select options for the other resulting prompts, as shown below.
Run the following command to navigate to the project directory:
cd paymentrequestapi-demo
Start the development server:
npm run dev
For the demo, we will build a simple cart page that shows a list of items, allows users to update the quantity of each item and includes a checkout button. The checkout button will trigger the Payment Request API, which will be integrated in the next section.
First, open the created project in your favorite code editor. Create a products.ts
file in the src/
folder and add the following to it:
export const initialProducts = [
{
id: 1,
name: "Brief Case",
amount: 250,
quantity: 1,
},
{
id: 2,
name: "Shoe",
amount: 100,
quantity: 1,
},
];
Here, we created an array of dummy products called initialProducts
.
Next, create a new components
folder in the src/
folder and add a Cart.ts
file to it:
"use client";
import React, { useState } from "react";
import { initialProducts } from "@/products";
export default function Cart() {
const [products, setProducts] = useState(initialProducts);
const updateQuantity = (id: number, value: number) => {
setProducts((prevProducts) =>
prevProducts.map((product) =>
product.id === id
? { ...product, quantity: Math.max(1, product.quantity + value) }
: product
)
);
};
const totalAmount = products.reduce(
(acc, product) => acc + product.amount * product.quantity,
0
);
const handleCheckout = () => {
console.log("Checkout Triggered!!");
};
return (
<section className="my-16 w-[95%] max-w-screen-sm border rounded p-4">
<h1 className="font-semibold text-lg text-center">
Static Cart - Payment Request API demo
</h1>
<div className="my-6 overflow-x-auto">
<table className="w-full border-collapse border text-left">
<thead>
<tr className="border-b">
<th className="p-2">Item</th>
<th className="p-2">Quantity</th>
<th className="p-2">Price</th>
</tr>
</thead>
<tbody>
{products.map((product) => (
<tr key={product.id} className="border-b">
<td className="py-4 px-2">{product.name}</td>
<td className="py-4 px-2">
<div className="flex gap-2">
<button onClick={() => updateQuantity(product.id, -1)}>
-
</button>
<span>{product.quantity}</span>
<button onClick={() => updateQuantity(product.id, 1)}>
+
</button>
</div>
</td>
<td className="py-4 px-2">
${product.amount * product.quantity}
</td>
</tr>
))}
</tbody>
</table>
<div className="flex justify-between items-center mt-8">
<p className="font-semibold text-lg">Total: ${totalAmount}</p>
<button
onClick={handleCheckout}
className="bg-green-700 py-2 px-6 text-white rounded font-semibold"
>
Checkout
</button>
</div>
</div>
</section>
);
}
Now, let’s break down what is happening in the code step by step.
First, we created a client component and defined a products
state variable. Then, we imported the initialProducts
array created earlier and passed it as the initial value of the products
state.
Next, we created a function called updateQuantity
that allows users to increase or decrease the number of items in the cart. Since this is just a demo, it won’t allow the quantity to go below 1.
Next, we computed the total cost of all the items in the cart by adding their prices and multiplying them by their quantities.
Then, we render the items in the cart in table format. Users can increase or decrease quantities using the +
and -
buttons. We also added a button that triggers a handleCheckout
function. For now, we log text to the console. From this point, we will trigger the Payment Request API in the next section.
Open the src/app/page.tsx
file and update the code as shown below:
import Cart from "@/components/Cart";
export default function Home() {
return (
<div className="w-full flex justify-center">
<Cart />
</div>
);
}
We replaced the template code in this file with the Cart
component we just created.
Now you can open the application in the browser and review what we have.
Now, let’s integrate the Web Payment API into our demo application. Create a util.ts
file in the src/
folder and add the following to it:
interface Product {
id: number;
name: string;
quantity: number;
amount: number;
}
export const checkout = async (products: Product[]) => {
try {
const methodData = [
{
supportedMethods: "https://google.com/pay",
data: {
environment: "TEST",
apiVersion: 2,
apiVersionMinor: 0,
merchantInfo: {
merchantName: "Example Merchant",
},
allowedPaymentMethods: [
{
type: "CARD",
parameters: {
allowedAuthMethods: ["PAN_ONLY", "CRYPTOGRAM_3DS"],
allowedCardNetworks: ["MASTERCARD", "VISA"],
},
tokenizationSpecification: {
type: "PAYMENT_GATEWAY",
parameters: {
gateway: "example",
gatewayMerchantId: "exampleGatewayMerchantId",
},
},
},
],
},
},
];
const displayItems = products.map((product) => ({
label: `${product.name} x${product.quantity}`,
amount: {
currency: "USD",
value: (product.amount * product.quantity).toFixed(2),
},
}));
const totalAmount = products.reduce(
(acc, product) => acc + product.amount * product.quantity,
0
);
const paymentDetails = {
displayItems,
total: {
label: "Total due",
amount: { currency: "USD", value: totalAmount.toFixed(2) },
},
};
const paymentRequest = new PaymentRequest(methodData, paymentDetails, {});
if (!(await paymentRequest.canMakePayment())) {
console.log("Payment Request API not available.");
return;
}
const paymentResponse = await paymentRequest.show();
await paymentResponse.complete("success");
alert("Payment successful!");
} catch (error) {
console.error("Payment failed:", error);
alert("Payment failed. Please try again.");
}
};
The code above creates a checkout
utility function that helps integrate the Web Payment API for transaction processing. First, we defined the accepted payment method, methodData
, which includes Google Pay. The definition used a test configuration since this is a demo application. You can always replace this with your live details.
The cart items are then formatted into displayItems
, and the total amount is calculated. With this data, a PaymentRequest
instance is created.
If everything goes well, the show
method is called to display the payment interface. If the user completes the payment, the response is handled and a success message is shown. On the other hand, if anything goes wrong, an error message is displayed.
Now, let’s go back to the Cart
component. Open the src/components/Cart.tsx
file and update the code as shown below:
"use client";
import React, { useState } from "react";
import { initialProducts } from "@/products";
import { checkout } from "@/util";
export default function Cart() {
const [products, setProducts] = useState(initialProducts);
const updateQuantity = (id: number, value: number) => {
//...
};
const totalAmount = products
.reduce
//...
();
const handleCheckout = () => {
if (window.PaymentRequest) {
checkout(products);
} else {
alert("You can't use this feature!");
}
};
return (
<section className="my-16 w-[95%] max-w-screen-sm border rounded p-4">
//...
</section>
);
}
Here, we imported the checkout
utility function and called it in the handleCheckout
function. Before making the call, we set the function to get triggered only when the Payment Request API feature is available.
Now, you can preview the changes in your browser.
In the previous section, we created the PaymentRequest
instance without initializing it with predefined options as the third and optional parameter. However, in this section, let’s examine one or two of the options before concluding.
Update the line where we instantiated PaymentRequest
in the src/util.ts
file as shown below:
const paymentRequest = new PaymentRequest(methodData, paymentDetails, {
requestPayerEmail: true,
requestShipping: true,
});
Here, we added the option to request the payer’s email and another to request the shipping information.
You can go ahead and hit the Checkout
button again in your browser to see what changed.
The Payment Request API is an important API that has contributed positively to the way payments are made on the web by providing a smooth, secure and user-friendly checkout experience across different platforms and devices.
In this article, we looked at the Payment Request API, its benefits and its use. Then, we walked through its implementation, from setting up a cart system to integrating the payment feature.
Chris Nwamba is a Senior Developer Advocate at AWS focusing on AWS Amplify. He is also a teacher with years of experience building products and communities.