Deep Dive into the Payment Request API

In this guide we'll explore the ins and outs of the Payment Request API, looking
at how our input affects the payment request UI, how we can request information
from the user (like name and phone number), and how the final payment and user
information is passed to your site.

If you are unsure of what the Payment Request API is or why it's useful, check
out the introduction
article.

One thing to keep in the back of your mind when working with the Payment Request
API is that the API manages the UI but performs no arithmetic; it will simply
display whatever input you pass to it. Throughout the examples we'll discuss
what this means for the developer and user.

Dogfood:PaymentRequest is still in development. While we think
it's stable enough to implement, it may continue to change. We'll keep this page updated
to always reflect the current status of the API. Meanwhile, to protect yourself
from API changes that may be backwards incompatible, we're offering a shim that
can be embedded on your site. The shim will paper over any API differences for
two major Chrome versions.

Feature Detect

Payment Request has been available since Chrome 53 for Android and has been
enabled by default since Chrome 61 on desktop. Before we start covering how
to use Payment Request, we should feature detect to ensure it's available and
fallback to a traditional checkout page in browsers that don't support it.

The constructor takes three arguments. The
first argument
defines which forms of payment you can
accept; for example, you may only accept 'visa' and 'mastercard'. The
paymentDetails
argument defines the total and display
items. The
optional third argument
is an object
used to request additional information from the user; for example, you can
request the payer's name, email address, and phone number.

All of these options affect the UI presented to the user as well as the amount
of information the browser will need to collect from them.

Constructing a new PaymentRequest object can be done at any point in your
app. Nothing will be shown to the user until you call its show() method.

const request = new PaymentRequest(
supportedPaymentMethods,
paymentDetails,
options
);
// Call when you wish to show the UI to the user.
request.show()
.then(...).catch(...);

Let's start by looking at the arguments we can pass into the PaymentRequest
constructor, starting with the supported payment methods.

Defining Supported Payment Methods

The Payment Request API is designed to support credit and debit card payments
as well as third party payment methods (such as Google Pay).

You must supply an array of objects indicating your supported payment methods
where each payment method must include a supportedMethods parameter that
identifies the payment method. Each object can contain an optional data
object.

If the user has no cards set up they'll be prompted to add details, otherwise
an existing card will be selected for them.

Note: To get access to all forms of payment available with Google,
developers will need to implement the Google Pay method. Refer to the
Google Pay API
docs for more information.

Example of basic-card support in the Payment Request API.

At the time of writing, Chrome supports amex, diners, discover, jcb,
mastercard, unionpay, mir, and visa, which you can see listed across the
top of the UI. Find out up to date list of approved card identifiers in the
spec.

To restrict the supported cards, we can add the optional data parameter and
define supportedNetworks. The following code restricts the accepted cards to
visa, mastercard and amex.

The payment request UI will restrict the accepted cards and the user will be
prevented from selecting other cards:

Example of reduced the list of supported cards in the Payment Request API.

In the screenshot above, the user must add a payment. If browsers have this
information readily available, they have the option of pre-selecting an
appropriate payment for the user. This means that the wider your card support,
the higher the odds are of the browser being able to pre-populate these details,
speeding up the checkout flow. For example, if I had a card saved in Chrome, the
UI would start with a suitable card already selected:

Cards will be preselected if available.

The supportedTypes parameter tells Chrome which types of cards to
filter out, i.e. if a merchant defined supportedTypes as ['credit', 'debit'],
Chrome would strip out any 'prepaid' cards the user has.

This means that supportedTypesdoes not guarantee that the final card
you receive will be a supported type. The user can enter details for a new card,
which could be an unsupported type, and the Payment Request API will allow this.
The supportedTypes option is just for filtering out existing cards.
Merchants still need to check the card type on their backend.

Chrome version 61 added support for the supportedTypes option. In older versions
of Chrome you would receive the following console warning:

Cannot yet distinguish credit, debit, and prepaid cards.

It's safe to use this option. The only difference is that some cards wouldn't
be filtered automatically for the user.

The reason for putting creditCardPaymentMethod into an array is to cater
for scenarios where a merchant supports multiple payment methods. For
example, let's say there was a payment processor called "BobPay" and you
(the merchant) accepted payments through BobPay as well as credit cards.
You'd define the supported payment methods like so:

Edge Cases

There are some edge cases to be aware of when defining your supported
payment methods.

Unsupported Payment Methods
If you try to call show() on a PaymentRequest object and there are no
supported payment methods, the returned promise will reject immediately with
the following error:

DOMException: The payment method is not supported

This shouldn't be a problem if you include 'basic-card' as a supported payment
method. If, however, you only support a third party payment method, like Google
Pay, there is a strong chance that it won't be supported by a browser
that supports the Payment Request API.

Third Party Payment Method Skipping the Payment Request UI
In the screenshot above you can see "Google Pay" as the pre-selected
payment option. This has occurred because the example supports both Google
Pay and basic cards. If you define Google Pay as your only payment
method and the browser supports it, and no additional data is requested from
PaymentOptions, the browser can (and Chrome does, at the time of writing)
skip the payment request UI altogether after the show() method is called
Users will be taken straight to Google Play services to complete the payment.

Defining Payment Details

The second argument we need to pass to the PaymentRequest constructor is
the payment details object. This object contains the total for the order and
an optional array of display items (i.e. a high level breakdown of the total).

Transaction Details: Total

The contents of the total parameter should contain a label parameter and
an amount parameter consisting of a currency and value. A basic example
would be:

The order of the items in the displayItems array will dictate their display
order in the UI.

Please bear in mind that displayItems are not designed to display a long
list of items. You should use this for high level entries instead of an itemized
list; for example, subtotal, discount, tax, and shipping cost.

It's worth repeating that the PaymentRequest API does not perform
any arithmetic. In the above example, all the item values do not add up
to the total. This is because we've set the total to have a value of zero.
It is the responsibility of your web app to calculate the correct
total.

Transaction Details: Display Items - Pending

If you have values that aren't final because they require additional
information, you can use the pending parameter.

An example use case for this is marking tax as a pending value until the user
has selected a shipping option, which may adjust the total and tax amounts.

Which will be given a slightly different text color in Chrome as a result:

Demonstration of a display item being marked as pending.

Edge Cases

There are some minor edge cases with the paymentDetails argument where
you can stress the UI and incorrectly define the object resulting in an error.

Long Total Label
Be wary of the length of the labels. Browsers have
control of how they are displayed. At the time of writing, Chrome doesn't
truncate the total label at all, but does truncate display item labels.

Long labels may result in a bad UI for users.

No Total
The total parameter is a required field; if you fail to include it, you'll
receive the following the error:

TypeError: Failed to construct 'PaymentRequest': Must specify total

Failing to include Label, Amount, Currency or Value
If you exclude a label, amount, currency, or value from the total
or one of the displayItems, you'll receive one of the following errors:

// No label
'PaymentRequest': required member label is undefined.
// No amount
'PaymentRequest': required member amount is undefined.
// No currency
'PaymentRequest': required member currency is undefined.
// No Value
'PaymentRequest': required member value is undefined.

If you hit any of these errors, please check the format of the total and
displayItems.

Negative Total (i.e., Refunds)
The Payment Request API does not support negative totals; if you attempt to
show a total with a negative value you'll receive this error:

'PaymentRequest': Total amount value should be non-negative

Invalid Currency
The currency code must be three uppercase characters; passing in anything else
will throw an error.

You can pass in any three characters and it will be treated as a valid currency
code.The reason for this is that it allows support for future currencies. For
example, Bitcoin can be supported with its currency code 'XBT'.

The currency code is always displayed in Chrome at the time of writing, but only
known currencies will include the currency character with amounts; otherwise only
the currency code is shown. Compare the screenshots below for 'USD' and 'XBT'.

Comparison of USD and XBT currencies on the payment request UI.

Multiple Currencies
At the time of writing, Chrome does not support multiple currencies and
unfortunately the error it throws does not make it clear that mixing
currencies is not supported.

Formatting Currency
You can define the value parameter as a string, but it must only contain
numbers with no more than one decimal point;
otherwise you'll receive the following error:

'PaymentRequest': '...' is not a valid amount format

For example, "1,000.00" is invalid because of the comma, whereas "1000.00" is
valid.

Right to Left Languages
You can use right-to-left languages for the labels and they will be displayed
accordingly. The rest of the browser's text will be determined by the user's
browser / system settings.

Defining Options (Optional*)

The third argument to the PaymentRequest constructor is the optional options
object.

This object should be used to define any additional information you require from
the user including the payer's name, email, and phone number. Only ask for what
you need, asking for more details will result in a longer checkout flow for the
user.

If you require this information you just need to pass in the following options
object:

By default, all of these values are false, but returning true for any of them
will result in an extra step in the UI.

Payment request UI requesting additional information from the user.

If the browser has these details available for the user, they'll be
pre-populated.

Pre-populated user information.

The options object is also used to configure shipping, but we'll look at
shipping in much more detail later on as it touches many parts of the API.

Edge Cases

The only edge case to note here is that if you define any of the parameters
(requestPayerName, requestPayerPhone or requestPayerEmail) with a
non-boolean value it will use the usual JavaScript
truthy /
falsy
value (i.e., null, undefined, 0 will be treated as false and 'string value',
{}, and [] will be treated as true).

Responding to PaymentRequest.show()

After calling show() the payment request UI will be displayed to the user.
Users will either close the UI or fill in the required fields and select 'Pay',
at which point your app will need to "complete" the transaction.

You'll know if the user has successfully filled in the details as the promise
returned by show() will resolve; if there was an issue or the user closed the
UI the promise will reject.

paymentRequest.show()
.then((paymentResponse) => {
// The user filled in the required fields and completed the flow
// Get the details from `paymentResponse` and complete the transaction.
return paymentResponse.complete();
})
.catch((err) => {
// The API threw an error or the user closed the UI
});

Once the user has filled in the payment request UI, your web app will receive a
PaymentResponse object in the show() promise.

Accessing Details from the PaymentResponse

This is the payment method selected by the user.
This will be one of the values passed into the `supportedMethods` objects
("basic-card" for example).

details

This is an object containing the payment details. The contents of this
object will depend on the selected payment method. In the next paragraph
we'll look at the contents of a basic card.

payerName

This will be null unless you set `requestPayerName` to true in the options
object, in which case this will be a string of the payer's name.

payerPhone

This will be null unless you set `requestPayerPhone` to true in the options
object, in which case this will be a string of the payer's phone number.

payerEmail

This will be null unless you set `requestPayerEmail` to true in the options
object, in which case this will be a string of the payer's email address.

* Shipping Info

We will cover this in the shipping section.

The details object is only standardized for the basic-card payment method. For
third party payment methods, like Google Pay, the details object's content
will be documented by the payment method.

For 'basic-card' payments, the details object will contain the billingAddress,
cardNumber, cardSecurityCode, cardholderName, expiryMonth, and expiryYear.

Once you have these details from the PaymentResponse, you need to process the
payment and "complete" the transaction by calling the paymentResponse.complete()
method.

Completing the Transaction

After the promise from show() has resolved, the payment request UI will display a
loading UI to the user. You can either leave this spinner visible while you
process the payment details or you can close the UI immediately and validate the
payment details in your own UI.

Possible flows after the show() promise has resolved.

If you wanted to close the payment request UI immediately, you would call
the PaymentResponse.complete() method.

This will close the payment request UI and you can do whatever you want to
process the user's payment information. Calling the complete() method
without any arguments like this, is equivalent to calling
paymentResponse.complete('unknown'). You are telling the browser to
not treat the payment as a success or failure and to show no UI or animations
to suggest otherwise to the user.

If you wanted to process the payment while the payment request UI is showing
a spinner you'd delay the call to complete(). Let's assume we have a method
called validatePaymentWithBackend() that will check the details with our
backend and return a promise resolving to a boolean (true if the payment was
successful, false otherwise). We'd keep the spinner up and call complete()
after this method has resolved, like so:

In this example we are using the 'success' and 'fail' strings to highlight
to the browser the states of the transaction. If you include these strings
the browser may show a visual indication to the user suggesting a positive
or negative outcome.

At the time of writing, Chrome will just hide the UI on success but will show
an error dialog to the user if you call with complete('fail') .

Example of the error dialog shown when calling complete with fail input.

Edge Cases

Completing with a Diff String
One possible gotcha with the complete() method is that if you pass in a
string that is not defined by the spec (i.e., the string is not 'unknown',
'success,' or 'fail'), the promise returned by complete() will reject and
the payment request UI will not close.
(This behavior will hopefully change
soon.)
The error in the rejected promise will be:

Failed to execute 'complete' on 'PaymentResponse': The provided value '...' is not a valid enum value of type PaymentComplete.

Not Calling Complete
If you fail to call complete() in a timely manner, it will time out and the
UI will be closed. The browser will show a message to the user highlighting
there was an issue.

Shipping in Payment Request API

When you are selling physical goods you'll need to collect shipping information
from your users.

In this section we'll look at how you can request shipping information from the
user, define the type of shipping, and react to changes to the shipping address
and shipping option from the user.

Request Shipping Details

To start off with, you'll need to request shipping information from the user,
which is achieved by setting requestShipping to true in the options object.

const options = {
requestShipping: true,
};

This will ask the user for their shipping address:

Request shipping information from the user.

By default the Payment Request API will set a shippingType to 'shipping'.
This is why the title on the highlighted row above is "Shipping".

Changing the Shipping Type

There are situations where the term "Shipping" isn't appropriate given the
current context, even though an address is required from the user. For example,
a food delivery service needs a delivery address and a laundry service may
need a pickup address.

To serve these use cases, you can define a shippingType of "shipping", "delivery",
or "pickup" to change the UI's title to "Shipping", "Delivery" and "Pickup".

There is a small lifecycle that the user will go through when selecting a
shipping address.

Lifecycle of shipping address selection.

The flow is:

The user selects the address input.

The user picks a predefined address or adds a new one.

Your app has the opportunity of validating the address details and defining
shipping options based on the selection.

The shipping options field will be enabled.

The user will be given the options you've defined.

Your app will have the option to validate the selection.

The user will be able to continue with the rest of the flow.

We'll start by looking at the shppingAddressChange event which is
dispatched as the user selects a new address.

Handling Shipping Address Changes

If you've requested shipping information from the user, a shippingAddressChange
event will be dispatched whenever the user changes the shipping address. This
gives you an opportunity to check that the address meets any requirements you
might have (e.g. you may not ship to specific countries) and it provides an
opportunity to determine what the available shipping options are.

At the time of writing, you are required to add a
shippingaddresschange
event listener in Chrome (although this behavior is likely to change to be
optional).

We are simply adding the event listener to our PaymentRequest instance before
calling show(). When this event is triggered, the common pattern will be to
check the user's selected address and then provide the available shipping
options.

Retrieving the shippingAddress

When a shippingAddressChange event has been dispatched, the selected address
will be added to the current PaymentRequest instance under the
shippingAddress parameter.

To give an example, we can print the shipping address to the console like so:

This is the name of the recipient or contact person.
This member may, under certain circumstances, contain multiline information.
For example, it might contain "care of" information.

organization

String

This is the organization, firm, company, or institution at this address.

addressLine

Array of string

This is the most specific part of the address. It can include, for example,
a street name, a house number, apartment number, a rural delivery route,
descriptive instructions, or a post office box number.

city

String

This is the city/town portion of the address.

region

String

This is the top level administrative subdivision of the country.
For example, this can be a state, a province, an oblast, or a prefecture.

This is the BCP-47 language code for the address. It's used to determine
the field separators and the order of fields when formatting the address
for display.

sortingCode

String

This is the sorting code as used in, for example, France.

dependentLocality

String

This is the dependent locality or sublocality within a city. For example,
used for neighborhoods, boroughs, districts, or UK dependent localities.

This should be everything you need to get the user's selected shipping address
but it does raise the question, how do you tell the Payment Request API what the
available shipping options are?

Defining the Available Shipping Options

The event object you'll receive in the shippingaddresschange event is a
PaymentRequestUpdateEvent which has a method updateWith(). You must call
this method with a paymentDetails object (i.e. an object with a total and
optional displayItems) or call it with a Promise that resolves to a
paymentDetails object.

The important difference when passing in a paymentDetails object to
event.updateWith() compared to passing it to the constructor, is that we
must include shippingOptions, which will be the array of objects defining
what the user can select.

For a simple example, we can call event.updateWith() with payment details of
a total and an empty array for shippingOptions:

Returning no shipping options tells the browser that the
selected shipping address is not suitable or supported by your site, so browsers
will display an error to the user:

Returning no shipping options displays an error to users.

This happens because the API expects at least one shipping option to be
available for the user to select. Before we look at how to actually define the
shipping options, it's worth covering how you can customize the error message.

Customizing the Shipping Options Error

If you can't ship to a user's selected address, you should provide a meaningful
message to them. Adding an error parameter to the
transaction details you return to event.updateWith() will achieve this.

Defining Shipping Options

Where there are available shipping options, you can define the available options
by setting the shippingOptions to an array of objects containing an id,
label, and amount with currency and value, like so:

This will give the user a set of options to select under the "Shipping Method"
title.

How shipping options are displayed to the user.

In the above examples, we are calling event.updateWith() directly with our
paymentDetails containing the total and shippingOptions. If we do this,
the user will not see a loading spinner and can continue with the rest of the
checkout flow.

If you need time to retrieve available shipping options from your backend, you
can pass a Promise to event.updateWith(). In the following example we are
making an API call and setting the shipping options based on the response:

Edge Cases

Never Calling updateWith() or Long Promise
If you fail to call event.updateWith() or pass it a promise that fails to
resolve in a timely manner, the UI will close itself and the show promise will
reject with the following error:

DOMException: Timed out as the page didn't resolve the promise from change event

Using an Invalid Shipping Type
If you set the shippingType to an invalid value (i.e., something other than
'shipping', 'delivery' or 'pickup'), you'll receive the following error:

The provided value '...' is not a valid enum value of type PaymentShippingType.

Shipping Options with a Non-Unique ID
If the returned shipping options do not have unique IDs
the Payment Request API will treat this as though the address is invalid. In
other words, it's the equivalent of setting the shippingOptions to an empty
array, which shows the "Can't ship to this address. Select a different address."
message.

Missing Information from a Shipping Option
If any of the shipping options are missing a required parameter then the
promise returned by show() will reject with an error highlighting the missing
parameter. Please note that it does not highlight where the value is missing
(i.e., the error message may be referring to a problem in the total,
displayItems, or shippingOptions parameters).

Handling Shipping Options Changes

Just as you can listen for changes to the shipping address, you can listen for
changes to the selected shipping option. The tasks to perform here are to mark
the shipping option as selected if it's valid and update the total and
display items.

We add a listener for shippingoptionchange events to our PaymentRequest
instance same as shippingaddresschange.

Doing this will update the UI as the user selects a new shipping option.
If you return a promise to updateWith(), the user will also be presented
with a spinner until it resolves.

The lifecycle of shipping option selection.

Edge Cases

All of the edge cases from the shippingaddresschange event apply to
shippingoptionschange event. There are a few additional edge cases to be
aware of.

Failing to Mark an Option as Selected
One footgun to be aware of is that if you fail to mark a shipping option
as selected, you will end up in a scenario where the user will be stuck in a
loop without any reason as to why they can't select an option. They can go
through the flow to select a shipping option, the event is triggered and if
nothing is marked as selected, they'll be taken back to the payment request UI
and have to select a shipping method again.

Selecting Multiple Options
If you set the selected parameter to true on multiple shipping options,
the last entry will be selected.

Variants to Above Flow

In the above flow, we took the user through the following steps:

The user selects an address.

The app validates the address / provides some shipping options.

The user selects a shipping option.

The app updates the total and display items.

The user completes the transaction.

You can shorten this flow for the user by specifying data at
different stages. In this section we'll see what we can change.

Defining Shipping Options Up Front

In the PaymentRequest constructor, we can define shippingOptions as
part of the initial paymentDetails object (i.e. we don't have to wait
until a shippingaddresschange event):

If we do this, there is no difference to the flow we discussed before.
The user will still need to select an address and then shipping options.
However, if you mark one of the shipping options as selected in this
initial object, the browser may pre-select an address. For example, if
we construct our payment request UI with the following input
(note the selected parameter):

The user will be presented with a UI where an address and shipping
has been selected.

An example of preselecting a shipping option in the PaymentRequest constructor.

This has the advantage that the user can click the 'Pay' button immediately
and complete the transaction with one click. The downside is that you won't
receive a shippingaddresschange event, meaning that you can't validate the
address details.

Only select a shipping option in the constructor like this if you are
extremely confident that the selected option won't need to change
based on the user's address (e.g. you offer worldwide free shipping).

Select a Shipping Option in shippingaddresschange

In the shippingaddresschange event we saw that whichever shipping options we
passed to event.updateWith() would be made available to the user.

Setting one of the options' selected parameter to true will select that option,
allowing the user to progress through the checkout flow quicker. This is perfect
if you know the most common shipping option.

In the above example, if we leave the payment request dialog visible,
the show() promise will reject with the following error:

DOMException: The user aborted a request.

The promise returned by abort() allows you to detect whether the abort was
successful or not. An example of where it might fail is if the user cancels
the payment request UI or completes the transaction before you call abort(),
although it can fail to abort when the developer is in the middle of entering
details.

The Payment Request UI

One aspect of the UI hasn't been discussed. How is the top section of the
payment request UI is constructed?

The generated piece of the payment request UI.

This UI is a combination of information readily available from the page.

The icon on the left is the most appropriately sized icon the browser can
find.

You can define multiple icon sizes in the head of your document like so:

Edge Cases

Query Quota Exceeded
Querying canMakePayment() with different payment methods will
result in a quota error:

DOMException: Query quota exceeded

The reason this error is thrown is to block attempts to fingerprint the
user.

At the time of writing, Chrome will reset the quota after 30 minutes or
when it's restarted.

Will it Ever Fail for Basic-Cards?
Basic cards are a standardized payment method of the Payment Request API,
so is there every a scenario where canMakePayment() will fail?

It can fail if the user has no known cards or the known cards are invalid
(i.e., the details for the card don't pass the
LUHN check).

How does canMakePayment() Work with Payment Apps?
If a site indicates support for URL payment method, like
'https://example.com/bobpay', canMakePayment() will return "false" if the
app is not installed. When the app is installed, it can decide if
canMakePayment() should return true or false.

Note: In incognito mode, the 3rd party app will not be queried and
canMakePayment() will always return "true".

PaymentRequest Shim

To mitigate the pains of catching up with this living standard API, we
strongly recommend you add
this shim
to the <head> of your page. This shim will be updated as the API changes
and will do its best to keep your code working for at least two major
releases of Chrome.