Using the Payment Request API
The Payment Request API provides a browser-based method of connecting users and their preferred payment systems and platforms to merchants that they want to pay for goods and services. This article is a guide to making use of the Payment Request API, with examples and suggested best practices.
The basics of making a payment
This section details the basics of using the Payment Request API to make a payment.
Creating a new payment request object
A payment request always starts with the creation of a new PaymentRequest
object — using the PaymentRequest()
constructor. This takes two mandatory parameters and one option parameter:
-
methodData
— an object containing information concerning the payment provider, such as what payment methods are supported, etc. -
details
— an object containing information concerning the specific payment, such as the total payment amount, tax, shipping cost, etc. -
options
(optional) — an object containing additional options related to the payment.
So for example, you could create a new PaymentRequest
instance like so:
const request = new PaymentRequest(
buildSupportedPaymentMethodData(),
buildShoppingCartDetails(),
);
The functions invoked inside the constructor return the required object parameters:
function buildSupportedPaymentMethodData() {
return [{ supportedMethods: "https://example.com/pay" }];
}
function buildShoppingCartDetails() {
return {
id: "order-123",
displayItems: [
{
label: "Example item",
amount: { currency: "USD", value: "1.00" },
},
],
total: {
label: "Total",
amount: { currency: "USD", value: "1.00" },
},
};
}
Starting the payment process
Once the PaymentRequest
object has been created, you call the PaymentRequest.show()
method on it to initiate the payment request. This returns a promise that fulfills with a PaymentResponse
object if the payment is successful:
request.show().then((paymentResponse) => {
paymentResponse.complete("success").then(() => {
introPanel.style.display = "none";
successPanel.style.display = "block";
});
});
This object provides the developer with access to details they can use to complete the logical steps required after the payment completes, such as an email address to contact the customer, a shipping address for mailing goods out to them, etc. In the code above, you'll see that we've called the PaymentResponse.complete()
method to signal that the interaction has finished — you'd use this to carry out finishing steps, like updating the user interface to tell the user the transaction is complete, etc.
Other useful payment request methods
There are some other useful payment request methods worth knowing about.
PaymentRequest.canMakePayment()
can be used to check whether the PaymentRequest
object is capable of making a payment before you start the payment process. It returns a promise that fulfills with a boolean indicating whether it is or not, for example:
new PaymentRequest(buildSupportedPaymentMethodData(), {
total: { label: "Stub", amount: { currency: "USD", value: "0.01" } },
})
.canMakePayment()
.then((result) => {
if (result) {
const request = new PaymentRequest(
buildSupportedPaymentMethodData(),
checkoutObject,
);
request.show().then((paymentResponse) => {
paymentResponse.complete("success").then(() => {
});
});
}
});
PaymentRequest.abort()
can be used to abort the payment request if required.
Detecting availability of the Payment Request API
You can effectively detect support for the Payment Request API by checking if the user's browser supports PaymentRequest
, i.e. if (window.PaymentRequest)
.
In the following snippet, a merchant page performs this check, and if it returns true
updates the checkout button to use PaymentRequest
instead of legacy web forms.
const checkoutButton = document.getElementById("checkout-button");
if (window.PaymentRequest) {
let request = new PaymentRequest(
buildSupportedPaymentMethodNames(),
buildShoppingCartDetails(),
);
checkoutButton.addEventListener("click", () => {
request
.show()
.then((paymentResponse) => {
})
.catch((error) => {
window.location.href = "/legacy-web-form-checkout";
});
request = new PaymentRequest(
buildSupportedPaymentMethodNames(),
buildShoppingCartDetails(),
);
});
}
Checking whether users can make payments
Checking whether users can make payments is always useful. Here's a couple of related techniques.
One useful technique to employ is customizing the payment request button depending on whether users can make payments.
In the following snippet we do just this — depending on whether the user can make a fast payment or needs to add payment credentials first, the title of the checkout button changes between "Fast Checkout with W3C" and "Setup W3C Checkout". In both cases, the checkout button calls PaymentRequest.show()
.
const checkoutButton = document.getElementById("checkout-button");
checkoutButton.innerText = "Loading…";
if (window.PaymentRequest) {
const request = new PaymentRequest(
buildSupportedPaymentMethodNames(),
buildShoppingCartDetails(),
);
request
.canMakePayment()
.then((canMakeAFastPayment) => {
checkoutButton.textContent = canMakeAFastPayment
? "Fast Checkout with W3C"
: "Setup W3C Checkout";
})
.catch((error) => {
checkoutButton.textContent = "Checkout with W3C";
});
}
Checking before all prices are known
If the checkout flow needs to know whether PaymentRequest.canMakePayment()
will return true
even before all line items and their prices are known, you can instantiate PaymentRequest
with dummy data and pre-query .canMakePayment()
. If you call .canMakePayment()
multiple times, keep in mind that the first parameter to the PaymentRequest
constructor should contain the same method names and data.
const supportedPaymentMethods = [
];
let shouldCallPaymentRequest = true;
let fallbackToLegacyOnPaymentRequestFailure = false;
new PaymentRequest(supportedPaymentMethods, {
total: { label: "Stub", amount: { currency: "USD", value: "0.01" } },
})
.canMakePayment()
.then((result) => {
shouldCallPaymentRequest = result;
})
.catch((error) => {
console.error(error);
shouldCallPaymentRequest = true;
fallbackToLegacyOnPaymentRequestFailure = true;
});
function onCheckoutButtonClicked(lineItems) {
callServerToRetrieveCheckoutDetails(lineItems);
}
function onServerCheckoutDetailsRetrieved(checkoutObject) {
if (shouldCallPaymentRequest) {
const request = new PaymentRequest(supportedPaymentMethods, checkoutObject);
request
.show()
.then((paymentResponse) => {
})
.catch((error) => {
console.error(error);
if (fallbackToLegacyOnPaymentRequestFailure) {
window.location.href = "/legacy-web-form-checkout";
} else {
showCheckoutErrorToUser();
}
});
} else {
window.location.href = "/legacy-web-form-checkout";
}
}
Recommending a payment app when user has no apps
If you select to pay with the BobPay demo payment provider on this merchant page, it tries to call PaymentRequest.show()
, while intercepting the NOTSUPPORTEDERR
exception. If this payment method is not supported, it redirects to the signup page for BobPay.
The code looks something like this:
checkoutButton.addEventListener("click", () => {
const request = new PaymentRequest(
buildSupportedPaymentMethodData(),
buildShoppingCartDetails(),
);
request
.show()
.then((paymentResponse) => {
paymentResponse.complete("success").then(() => {
introPanel.style.display = "none";
successPanel.style.display = "block";
});
})
.catch((error) => {
if (error.code === DOMException.NOT_SUPPORTED_ERR) {
window.location.href = "https://bobpay.xyz/#download";
} else {
introPanel.style.display = "none";
legacyPanel.style.display = "block";
}
});
});
Showing additional user interface after successful payments
If the merchant desires to collect additional information not part of the API (e.g., additional delivery instructions), the merchant can show a page with additional <input type="text">
fields after the checkout.
request
.show()
.then((paymentResponse) => {
paymentResponse.complete('success').then(() => {
const additionalDetailsContainer = document.getElementById('additional-details-container');
additionalDetailsContainer.style.display = 'block';
window.scrollTo(additionalDetailsContainer.getBoundingClientRect().x, 0);
})
.catch((error) => {
});
Pre-authorizing transactions
Some use cases (e.g., paying for fuel at a service station) involve pre-authorization of payment. One way to do this is through a Payment Handler (see the Payment Handler API). At time of writing, that specification includes a CanMakePayment
event that a Payment Handler could make use of to return authorization status.
The merchant code would look like this:
const paymentRequest = new PaymentRequest(
[{ supportedMethods: "https://example.com/preauth" }],
details,
);
paymentRequest
.canMakePayment()
.then((res) => {
if (res) {
} else {
}
})
.catch((err) => {
});
The payment handler would include the following code:
self.addEventListener("canmakepayment", (evt) => {
const preAuthSuccess = true;
evt.respondWith(preAuthSuccess);
});
This payment handler would need to live in a service worker at https://example.com/preauth
scope.
See also