Payment Request API: an Integration Guide

Buying goods online is a convenient but often frustrating experience, particularly on mobile devices. Although mobile traffic continues to increase, mobile conversions account for only about a third of all completed purchases. In other words, users abandon mobile purchases twice as often as desktop purchases. Why?

Why users abandon mobile purchase forms

Online purchase forms are user-intensive, difficult to use, slow to load and refresh, and require multiple steps to complete. This is because two primary components of online payments—security and convenience—often work at cross-purposes; more of one typically means less of the other.

Most of the problems that lead to abandonment can be directly traced to purchase forms. Each app or site has its own data entry and validation process, and users often find they must enter the same information at every app's purchase point. Also, application developers struggle to create purchase flows that support multiple unique payment methods; even small differences in payment method requirements can complicate the form completion and submission process.

Any system that improves or solves one or more of those problems is a welcome change. We started solving the problem already with Autofill, but now we'd like to talk about a more comprehensive solution.

Introducing the Payment Request API

The Payment Request API is a system that is meant to eliminate checkout forms. It vastly improves user workflow during the purchase process, providing a more consistent user experience and enabling web merchants to easily leverage disparate payment methods. The Payment Request API is not a new payment method, nor does it integrate directly with payment processors; rather, it is a process layer whose goals are:

  • To let the browser act as intermediary among merchants, users, and payment methods
  • To standardize the payment communication flow as much as possible
  • To seamlessly support different secure payment methods
  • To work on any browser, device, or platform—mobile or otherwise

The Payment Request API is an open and cross-browser standard that replaces traditional checkout flows by allowing merchants to request and accept any payment in a single API call. The Payment Request API allows the web page to exchange information with the user agent while the user is providing input, before approving or denying a payment request.

Best of all, with the browser acting as an intermediary, all the information necessary for a fast checkout can be stored in the browser, so users can just confirm and pay, all with a single click.

Payment transaction process

Using the Payment Request API, the transaction process is made as seamless as possible for both users and merchants.

The payment transaction process

The process begins when the merchant site creates a new PaymentRequest and passes to the browser all the information required to make the purchase: the amount to be charged, what currency they expect payment in, and what payment methods are accepted by the site. The browser determines compatibility between the accepted payment methods for the site and the methods the user has installed on the target device.

Payment Request Interface

The browser then presents the payments UI to the user, who selects a payment method and authorizes the transaction. A payment method can be as straightforward as a credit card that is already stored by the browser, or as esoteric as third-party application written specifically to deliver payments to the site (this functionality is coming soon). After the user authorizes the transaction, all the necessary payment details are sent directly back to the site. For example, for a credit card payment, the site will get back a card number, a cardholder name, an expiration date, and a CVC.

Payment Request can also be extended to return additional information, such as shipping addresses and options, payer email, and payer phone. This allows you to get all the information you need to finalize a payment without ever showing the user a checkout form.

The beauty of the new process is threefold: from the user's perspective, all the previously tedious interaction—request, authorization, payment, and result—now takes place in a single step; from the website's perspective, it requires only a single JavaScript API call; from the payment method's perspective, there is no process change whatsoever.

Using the Payment Request API

Load Payment Request API shim

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

<script src="https://storage.googleapis.com/prshim/v1/payment-shim.js">

Create a PaymentRequest

The first step is to create a PaymentRequest object by calling the PaymentRequest constructor. This step is typically (but not always) associated with a user-initiated action indicating their intent to make a purchase. The object is constructed using parameters that contain required data.

var request = new PaymentRequest(
  methodData, // required payment method data
  details,    // required information about transaction
  options     // optional parameter for things like shipping, etc.
);

PaymentRequest constructor

The methodData parameter

The methodData parameter contains a list of supported payment methods and, if relevant, additional information about the payment method. This sequence contains PaymentMethodData dictionaries, including standard identifiers that are associated with the payment methods the app intends to accept, and any payment method-specific data. See Payment Request API Architecture for more details.

Right now, PaymentRequest in Chrome only supports the following standard credit cards: 'amex', 'diners', 'discover', 'jcb', 'maestro', 'mastercard', 'unionpay', and 'visa'.

var methodData = [
  {
    supportedMethods: ["visa", "mastercard"]
  }
]

Payment methods and data

The details parameter

The details parameter contains information about the transaction. There are two major components: a total, which reflects the total amount and currency to be charged, and an optional set of displayItems that indicate how the final amount was calculated. This parameter is not intended to be a line-item list, but is rather a summary of the order's major components: subtotal, discounts, tax, shipping costs, etc.

Payment Request Interface

It is important to note that the Payment Request API does not do arithmetic. That is, it does not and cannot ensure that the display components correctly sum to the total amount due. These calculations are the developer's responsibility. So you should always ensure that the list items sum to the same amount in the total. Also, PaymentRequest doesn't support refunds, so the amounts should always be positive (but individual list items can be negative, such as discounts).

The browser will render the labels as you define them and automatically render the correct currency formatting based on the user's locale. Note that the labels should be rendered in the same language as your content.

var details = {
  displayItems: [
    {
      label: "Original donation amount",
      amount: { currency: "USD", value : "65.00" }, // US$65.00
    },
    {
      label: "Friends and family discount",
      amount: { currency: "USD", value : "-10.00" }, // -US$10.00
      pending: true // The price is not determined yet
    }
  ],
  total:  {
    label: "Total",
    amount: { currency: "USD", value : "55.00" }, // US$55.00
  }
}

Transaction details

pending is commonly used to show items such as shipping or tax amounts that depend upon selection of shipping address or shipping option. Chrome indicates pending fields in the UI for the payment request.

Repeated or calculated values used in the details can be specified either as string literals or as individual string variables.

var currency = "USD";
var amount = "65.00";
var discount = "-10.00";
var total = "55.00";

PaymentRequest variables

Display the PaymentRequest

Payment Request Interface

Activate the PaymentRequest interface by calling its show() method. This method invokes a native UI that allows the user to examine the details of the purchase, add or change information, and finally, pay. A Promise (indicated by its then() method and callback function) that resolves will be returned when the user accepts or rejects the payment request.

request.show().then(function(paymentResponse) {
  // Process paymentResponse here
  paymentResponse.complete("success");
}).catch(function(err) {
  console.error("Uh oh, something bad happened", err.message);
});

PaymentRequest show method

Abort a Payment Request

You can intentionally abort a PaymentRequest by calling its abort() method. This is particulary useful when the shopping session is timed out or an item in the cart sells out during the transaction.

Use this method if the app needs to cancel the payment request after the show() method has been called but before the promise has been resolved — For example, if an item is no longer available, or the user fails to confirm the purchase within an allotted amount of time.

If you abort a request, you'll need to create a new instance of PaymentRequest before you can call show() again.

var paymentTimeout = window.setTimeout(function() {
  window.clearTimeout(paymentTimeout);
  request.abort().then(function() {
    console.log('Payment timed out after 20 minutes.');
  }).catch(function() {
    console.log('Unable to abort.');
  });
}, 20 * 60 * 1000);  /* 20 minutes */

PaymentRequest abort method

Process the PaymentResponse

Upon a user approval for a payment request, the show() method's promise resolves, resulting in a PaymentResponse object.

PaymentResponse has the following fields:
methodName A string indicating what the chosen payment method is (e.g., visa)
details A dictionary containing information for methodName
shippingAddress The shipping address of the user, if requested
shippingOption The ID of the selected shipping option, if requested
payerEmail The email address of the payer, if requested
payerPhone The phone number of the payer, if requested
payerName The name of the payer, if requested

For credit card payments, the response is standardized. For non-credit card payments (e.g., Android Pay), the response will be documented by the provider. A credit card response contains the following dictionary:

cardholderName cardNumber expiryMonth expiryYear cardSecurityCode billingAddress

After payment information is received, the app should submit the payment information to your payment processor for processing. The UI will show a spinner while the request takes place. When a response has come back, the app should call complete() to close the UI.

request.show().then(paymentResponse => {
  var paymentData = {
    // payment method string, e.g. “visa”
    method: paymentResponse.methodName,
    // payment details as you requested
    details: paymentResponse.details
  };
  return fetch('/pay', {
    method: 'POST',
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(paymentData)
  }).then(res => {
    if (res.status === 200) {
      return res.json();
    } else {
      throw 'Payment Error';
    }
  }).then(res => {
    paymentResponse.complete("success");
  }, err => {
    paymentResponse.complete("fail");
  });
}).catch(err => {
  console.error("Uh oh, something bad happened", err.message);
});
Payment Request Interface

The complete() method tells the user agent that the user interaction is over and allows the app to notify the user of the result and to address the disposition of any remaining UI elements.

paymentResponse.complete('success').then(() => {
  // Success UI
}

paymentResponse.complete('fail').then(() => {
  // Error UI
};

PaymentRequest complete method

Collecting a shipping address

Payment Request Interface

If you are a merchant selling physical goods, you may want to collect the user's shipping address using the Payment Request API. This is accomplished by adding requestShipping: true to the options parameter. With this parameter set, "Shipping" will be added to the UI, and users can select from a list of stored addresses or add a new shipping address.

You can alternatively use "Delivery" or "Pickup" instead of "Shipping" in the UI by specifying shippingType. This is solely for display purposes.

var options = {
  requestShipping: true,
  shippingType: "shipping" // "shipping"(default), "delivery" or "pickup"
};

var request = new PaymentRequest(methodData, details, options);

Transaction options

Payment Request Interface

Shipping options can be dynamically calculated whenever a user selects or adds a new shipping address. You can add an event listener for the shippingaddresschange event, which fires on user selection of a shipping address. You can then validate the ability to ship to that address, calculate shipping options, and update your detailsshippingOptions with the new shipping options and pricing information. You can offer a default shipping option by setting selected to true on an option.

Beware, if you specify requestShipping: true, then you must define an event listener for the shippingaddresschange event, even if the shipping price does not change based on the address. Ignoring the event will eventually abort the payment even if you are not making any changes.

In order to reject an address for reasons such as non-supported region, pass details.shippingOptions an empty array. The UI will tell the user that the selected address is not available for shipping.

request.addEventListener('shippingaddresschange', e => {
  e.updateWith(((details, addr) => {
    if (addr.country === 'US') {
      var shippingOption = {
        id: '',
        label: '',
        amount: {currency: 'USD', value: '0.00'},
        selected: true
      };
      if (addr.region === 'US') {
        shippingOption.id = 'us';
        shippingOption.label = 'Standard shipping in US';
        shippingOption.amount.value = '0.00';
        details.total.amount.value = '55.00';
      } else {
        shippingOption.id = 'others';
        shippingOption.label = 'International shipping';
        shippingOption.amount.value = '10.00';
        details.total.amount.value = '65.00';
      }
      if (details.displayItems.length === 2) {
        details.displayItems.splice(1, 0, shippingOption);
      } else {
        details.displayItems.splice(1, 1, shippingOption);
      }
      details.shippingOptions = [shippingOption];
    } else {
      details.shippingOptions = [];
    }
    return Promise.resolve(details);
  })(details, request.shippingAddress));
});
Payment Request Interface

Upon user approval for a payment request, the show() method's promise resolves. The app may use the shippingAddress property of the PaymentResponse object to inform the payment processor of the shipping address, along with other properties.

request.show().then(paymentResponse => {
  var paymentData = {
    // payment method string
    method: paymentResponse.methodName,
    // payment details as you requested
    details: paymentResponse.details.toJSON(),
    // shipping address information
    address: paymentResponse.shippingAddress.toJSON()
  };
  // Send information to the server
});

Adding shipping options

If your service allows users to select shipping options such as "free", "standard", or "express", you can also do that through Payment Request UI. To offer such choices, add the shippingOptions property and its options to the details object. By setting one choice to selected: true, the UI will render it as pre-selected (which means your total amount should reflect the price for that shipping option).

var details = {
  total: {label: 'Donation', amount: {currency: 'USD', value: '55.00'}},
  displayItems: [
    {
      label: 'Original donation amount',
      amount: {currency: 'USD', value: '65.00'}
    },
    {
      label: 'Friends and family discount',
      amount: {currency: 'USD', value: '-10.00'}
    }
  ],
  shippingOptions: [
    {
      id: 'standard',
      label: 'Standard shipping',
      amount: {currency: 'USD', value: '0.00'},
      selected: true
    },
    {
      id: 'express',
      label: 'Express shipping',
      amount: {currency: 'USD', value: '12.00'}
    }
  ]
};
var request = new PaymentRequest(methodData, details, options);

Changing shipping options may have different prices. In order to add the shipping fee and change the total price, you may add an event listener for the shippingoptionchange event, which fires on user selection of a shipping option, so that you can run a programmatic examination of the option data. You may change the shipping fee depending on the shipping address as well.

request.addEventListener('shippingoptionchange', e => {
  e.updateWith(((details, shippingOption) => {
    var selectedShippingOption;
    var otherShippingOption;
    if (shippingOption === 'standard') {
      selectedShippingOption = details.shippingOptions[0];
      otherShippingOption = details.shippingOptions[1];
      details.total.amount.value = '55.00';
    } else {
      selectedShippingOption = details.shippingOptions[1];
      otherShippingOption = details.shippingOptions[0];
      details.total.amount.value = '67.00';
    }
    if (details.displayItems.length === 2) {
      details.displayItems.splice(1, 0, selectedShippingOption);
    } else {
      details.displayItems.splice(1, 1, selectedShippingOption);
    }
    selectedShippingOption.selected = true;
    otherShippingOption.selected = false;
    return Promise.resolve(details);
  })(details, request.shippingOption));
});
Payment Request Interface

Upon user approval for a payment request, the show() method's promise resolves. The app may use the shippingOption property of the PaymentResponse object to inform the payment processor of the shipping option, along with other properties.

request.show().then(paymentResponse => {
  var paymentData = {
    // payment method string
    method: paymentResponse.methodName,
    // payment details as you requested
    details: paymentResponse.details.toJSON(),
    // shipping address information
    address: paymentResponse.shippingAddress.toJSON(),
    // shipping option
    shippingOption: paymentResponse.shippingOption
  };
  // Send information to the server
});

Adding optional contact information

You can also collect a user's email address, phone number or name by configuring the options object.

var options = {
  requestPayerPhone: true,  // Request user's phone number
  requestPayerEmail: true,  // Request user's email address
  requestPayerName:  true   // Request user's name
};

var request = new PaymentRequest(methodData, details, options);
Payment Request Interface

Upon user approval for a payment request, the show() method's promise resolves. The app may use the payerPhone, payerEmail and/or payerName properties of the PaymentResponse object to inform the payment processor of the user choice, along with other properties.

request.show().then(paymentResponse => {
  var paymentData = {
    // payment method string
    method: paymentResponse.methodName,
    // payment details as you requested
    details: paymentResponse.details.toJSON(),
    // shipping address information
    address: paymentResponse.shippingAddress.toJSON(),
    // shipping option string
    shippingOption: paymentResponse.shippingOption,
    // payer's phone number string
    phone: paymentResponse.payerPhone,
    // payer's email address string
    email: paymentResponse.payerEmail,
    // payer's name string
    name: paymentResponse.payerName
  };
  // Send information to the server
});

Making PaymentRequest a progressive enhancement

As Payment Request API is an emerging feature, many browsers don't yet support it. To determine whether the feature is available, query window.PaymentRequest.

if (window.PaymentRequest) {
  // PaymentRequest supported
  // Continue with PaymentRequest API
} else {
  // PaymentRequest NOT supported
  // Continue with existing form based solution
}

Check payment method availability

Before calling show() and showing the PaymentRequest UI, you can optionally check to see if the user has a payment method available for payment. This gives developers more control over the final user experience they want to provide. To do so, use canMakePayment().

// Check if `canMakePayment()` exists as older Chrome versions
// don't support it.
if (request.canMakePayment) {
  request.canMakePayment().then(result => {
    if (result) {
      request.show();
    } else {
      // The user doesn't have an active payment method.
      // Forwarding to legacy form based experience
      location.href = '/checkout';
    }
  }).catch(error => {
    // Unable to determine.
    request.show();
  });
} else {
  request.show();
}

Putting them all together

function onBuyClicked(event) {
  if (!window.PaymentRequest) {
    // Payment Request API is not available. Forwarding to
    // legacy form based experience
    location.href = '/checkout';
    return;
  }
  // Payment Request API is available.
  // Stop the default anchor redirect.
  event.preventDefault();

  var supportedInstruments = [{
    supportedMethods: [
      'visa', 'mastercard', 'amex', 'discover', 'maestro',
      'diners', 'jcb', 'unionpay', 'bitcoin'
    ]
  }];

  var details = {
    displayItems: [{
      label: 'Original donation amount',
      amount: { currency: 'USD', value: '65.00' }
    }, {
      label: 'Friends and family discount',
      amount: { currency: 'USD', value: '-10.00' }
    }],
    total: {
      label: 'Total due',
      amount: { currency: 'USD', value : '55.00' }
    }
  };

  var options = {
    requestShipping: true,
    requestPayerEmail: true,
    requestPayerPhone: true,
    requestPayerName: true
  };

  // Initialization
  var request = new PaymentRequest(supportedInstruments, details, options);

  // When user selects a shipping address
  request.addEventListener('shippingaddresschange', e => {
    e.updateWith(((details, addr) => {
      var shippingOption = {
        id: '',
        label: '',
        amount: { currency: 'USD', value: '0.00' },
        selected: true
      };
      // Shipping to US is supported
      if (addr.country === 'US') {
        shippingOption.id = 'us';
        shippingOption.label = 'Standard shipping in US';
        shippingOption.amount.value = '0.00';
        details.total.amount.value = '55.00';
      // Shipping to JP is supported
      } else if (addr.country === 'JP') {
        shippingOption.id = 'jp';
        shippingOption.label = 'International shipping';
        shippingOption.amount.value = '10.00';
        details.total.amount.value = '65.00';
      // Shipping to elsewhere is unsupported
      } else {
        // Empty array indicates rejection of the address
        details.shippingOptions = [];
        return Promise.resolve(details);
      }
      // Hardcode for simplicity
      if (details.displayItems.length === 2) {
        details.displayItems[2] = shippingOption;
      } else {
        details.displayItems.push(shippingOption);
      }
      details.shippingOptions = [shippingOption];

      return Promise.resolve(details);
    })(details, request.shippingAddress));
  });

  // When user selects a shipping option
  request.addEventListener('shippingoptionchange', e => {
    e.updateWith(((details) => {
      // There should be only one option. Do nothing.
      return Promise.resolve(details);
    })(details));
  });

  // Show UI then continue with user payment info
  request.show().then(result => {
    // POST the result to the server
    return fetch('/pay', {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(result.toJSON())
    }).then(res => {
      // Only if successful
      if (res.status === 200) {
        return res.json();
      } else {
        throw 'Failure';
      }
    }).then(response => {
      // You should have received a JSON object
      if (response.success == true) {
        return result.complete('success');
      } else {
        return result.complete('fail');
      }
    }).then(() => {
      console.log('Thank you!',
          result.shippingAddress.toJSON(),
          result.methodName,
          result.details.toJSON());
    }).catch(() => {
      return result.complete('fail');
    });
  }).catch(function(err) {
    console.error('Uh oh, something bad happened: ' + err.message);
  });
}

// Assuming an anchor is the target for the event listener.
document.querySelector('#start').addEventListener('click', onBuyClicked);