Chrome Dev Summit 2018 is happening now and streaming live on YouTube. Watch now.

Web based payment apps developer guide

The Payment Request API brought to the web a native browser-based interface that allows users to enter required purchase information easier than ever before. The API can also invoke payment apps that provide various kinds of payment methods such as e-money, cryptocurrency, bank transfers, and more.

The Payment Handler API is an emerging web standard that enables websites to act as a payment handler. With other related specifications like Payment Method Identifier and Payment Method Manifest, the Payment Handler API can be integrated into and made available through the Payment Request API. This document describes how you can implement a web-based payment app using the Payment Handler API.

Overview

A payment app built with the Payment Handler API includes multiple endpoints that return specific content to a request. This chart shows example URL mappings of endpoints used in this document.

path contents
/ The root path of a web-based payment app that registers a service worker and a payment handler.
/manifest.json A web app manifest that defines the web-based payment app.
/payment-manifest.json A payment method manifest that defines how a payment method acts.
/service-worker.js JavaScript code that handles a payment request.
/pay A Payment Method Identifier URL that returns an HTTP header pointing to the payment method manifest.
/checkout The actual checkout page exposed to users.

Define a payment method identifier

A payment method identifier can be specified as a URL such as https://bobpay.xyz/pay that is used on a merchant website.

const request = new PaymentRequest([{
  supportedMethods: 'https://bobpay.xyz/pay'
}], {
  total: {
    label: 'total',
    amount: { value: '10', currency: 'USD' }
  }
});

The browser sends a HEAD HTTP request to the URL, which responds to the request with status code 200 OK or 204 No Content plus the URL of the payment method manifest. For example, if the manifest is at https://bobpay.xyz/payment-manifest.json, the response might look like this.

Link: <https://bobpay.xyz/payment-manifest.json>; rel="payment-method-manifest"

The URL can be a fully-qualified domain name or a relative path. Inspect https://bobpay.xyz/pay/ for network traffic to see an example. You can do this from a command line using the curl command:

curl --head https://bobpay.xyz/pay

Locate a payment method manifest

A payment method manifest is a JSON file that resides on your web server and defines which payment apps can use this payment method. A manifest file might look like this.

{
  "default_applications": ["https://bobpay.xyz/manifest.json"],
  "supported_origins": [
    "https://alicepay.friendsofalice.example"
  ]
}

Important properties:

  • default_applications: An array of absolute, fully-qualified URLs that points to web app manifests where the payment apps are hosted. This array is expected to reference the development manifest, production manifest, etc.
  • supported_origins: An array of URLs that points to origins that may host third-party payment apps implementing the same payment method. Note that a payment method can be supported by multiple payment apps. Use * to indicate that any origin can host the third-party payment apps.

Locate a web app manifest

Because the payment app acts as a Progressive Web App (PWA), you need a web app manifest file similar to this one.

{
  "name": "Pay with Bobpay",
  "short_name": "Bobpay",
  "description": "This is an example of the Payment Handler API.",
  "icons": [
    {
      "src": "images/manifest/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "images/manifest/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "serviceworker": {
    "src": "service-worker.js",
    "scope": "/",
    "use_cache": false
  },
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#3f51b5",
  "background_color": "#3f51b5"
}

Important properties:

  • name: Used as the payment app name.
  • icons: Used as the payment app icon. Only Chrome uses these icons; other browsers may use them as fallback icons if you don't specify them as part of the payment instrument.
Pay with Bobpay
The web app manifest's name property is used as the payment app name

Name the file manifest.json and place it on the web server, typically in the root folder, such as https://www.example.com/manifest.json. Point to the file from the HTML page that identifies it as part of the payment app.

<link rel="manifest" href="/manifest.json">

Chrome downloads this manifest at the time of payment app installation, so the page that installs the payment app (see next section) should contain this <link> element.

Install a payment app and set a payment instrument

Include the following JavaScript as part of the page that installs a service worker.

// Check if service worker is available 
if ('serviceWorker' in navigator) {
  // Register a service worker
  const registration = await navigator.serviceWorker.register(
    // A service worker JS file is separate
    'service-worker.js'
  );
  // Check if Payment Handler is available
  if (!registration.paymentManager) return;

  registration.paymentManager.userHint = 'payment-handler user hint';
  registration.paymentManager.instruments.set(
    // Payment instrument key can be any string.
    "bobpay-payment-method",
    // Payment instrument detail
    {
      name: 'Payment Handler Example',
      method: 'https://bobpay.xyz/pay'
    }
  )
}

Let's examine this code in more detail.

Register a service worker

Assuming you have a separate JavaScript file named service-worker.js, register the file as the source of the service worker. Be mindful of the service worker scope concept.

// Check if service worker is available  
if ('serviceWorker' in navigator) {  
  // Register a service worker  
  const registration = await navigator.serviceWorker.register(  
    // A service worker JS file is separate  
    'service-worker.js'  
  );

Set a payment instrument

Service worker registration returns a promise. Continue registering a payment instrument using the payment manager.

  // When service worker registration is done,
  // you'll receive a service worker registration object
  // (https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration).

  // Check if Payment Handler is available
  if (!registration.paymentManager) return;

  // `userHint` will be an exposed next to the name of the
  // payment app. The user hint should be user specific.
  // For example, this can be user email address.
  // If the user is on a public computer, be sure to clear the user
  // specific data after user logs out.
  registration.paymentManager.userHint = 'payment-handler user hint';

As shown above, registration.paymentManager.userHint can optionally set your payment app's hint. This is handy when you want to show some dynamic text like Visa ****4242.

User Hint
userHint will be used as second string in payment app interface.
  registration.paymentManager.instruments.set(
    // Payment instrument key
    "https://bobpay.xyz",
    // Payment instrument detail
    {
      // This parameter will be ignored in Chrome
      name: 'Payment Handler Example',
      // This parameter will be used to match against
      // the PaymentRequest.
      method: 'https://bobpay.xyz/pay'
    }
  );

You can call registration.paymentManager.instrument.set() to set your app's details. It accepts two arguments, instrumentKey and paymentInstrument.

registration.paymentManager.instruments.set(instrumentKey, paymentInstrument)

Set a string that identifies the payment instrument as the first argument. This can be any arbitrary string, and is used to identify instruments when you want to update them. Therefore, we recommended that you use an identifier from your payment app backend.

Set an object containing the payment instrument details (PaymentInstrument) as the second argument.

  • name: Set a string as the instrument name. This is ignored in Chrome, which uses the web app manifest’s name property, but other browsers may use it.
  • icons: Set an array of ImageObjects the Payment Request sheet will display. This is ignored in Chrome, which uses the web app manifest's icons property, but other browsers may use it.
  • method: A supported payment method identifier.
  • capabilities: Set the payment method specific parameters as an object. As of August 2018, basic-card is the only payment method that accepts capabilities. See the following example.
const request = new PaymentRequest([{ 
  supportedMethods: 'basic-card',
  data: {
    supportedNetworks: ['visa','master','jcb'],
    supportedTypes: ['credit','debit','prepaid']
  }
}], {
  total: {
    label: 'total',
    amount: {value: '10', currency: 'USD'}
  }
});
Manifest icons
The web app manifest's icons will be used as payment app icon.

Optional: Let Chrome install your payment app just-in-time

Chrome includes a feature to find and install a payment app just-in-time (JIT) when no supported payment apps have yet been installed for a payment request. Note that this is not a standardized feature, so other browsers may not support it.

To enable JIT installation for a payment app, it must be listed in default_applications in a corresponding payment method manifest, and the payment app's manifest must have a serviceworker section to indicate where to get the service-worker.js and its registration scope (as well as its name and icon).

JIT-installable payment apps are presented to the user when the payment request dialog is shown. After the user chooses an installable payment app and clicks "Pay", the payment app is installed and then a PaymentRequestEvent is sent to it. Note that CanMakePaymentEvent will not be sent to a JIT-installable payment app because the event happens before the payment request dialog is shown.

After the installation, the JIT-installable payment app is set as the supported payment method. The payment app can update it later as a previously-installed payment app.

Write a service worker

After the service worker is installed and the payment instrument is set, you are ready to accept payment requests. Here's an example service-worker.js.

const origin = 'https://bobpay.xyz';
const methodName = `${origin}/pay`;
const checkoutURL = `${origin}/checkout`;
let resolver;
let payment_request_event;

self.addEventListener('paymentrequest', e => {
  // Preserve the event for future use
  payment_request_event = e;
  // You'll need a polyfill for `PromiseResolver`
  // As it's not implemented in Chrome yet.
  resolver = new PromiseResolver();

  e.respondWith(resolver.promise);
  e.openWindow(checkoutURL).then(client => {
    if (client === null) {
      resolver.reject('Failed to open window');
    }
  }).catch(err => {
    resolver.reject(err);
  });
});

self.addEventListener('message', e => {
  console.log('A message received:', e);
  if (e.data === "payment_app_window_ready") {
    sendPaymentRequest();
    return;
  }

  if (e.data.methodName === methodName) {  
    resolver.resolve(e.data);  
  } else {  
    resolver.reject(e.data);  
  }  
});

const sendPaymentRequest = () => {
  if (!payment_request_event) return;
  clients.matchAll({
    includeUncontrolled: false,
    type: 'window'
  }).then(clientList => {
    for (let client of clientList) {
      client.postMessage(payment_request_event.total);
    }
  });
}

Let's break down this code block as well.

Receive a paymentrequest event and open a window

When a user selects your payment method and presses "Pay", the service worker receives a paymentrequest event. To receive it, add an event listener.

// `self` is global object in service worker
self.addEventListener('paymentrequest', e => {
  // Preserve the event for future use
  payment_request_event = e;
  // You'll need a polyfill for `PromiseResolver`
  // As it's not implemented in Chrome yet.
  resolver = new PromiseResolver();

  // Pass a promise that resolves when payment is done.
  e.respondWith(resolver.promise);
  // Open the checkout page.
  e.openWindow(checkoutURL).then(client => {
    if (client === null) {
      // Make sure to reject the promise on failure
      resolver.reject('Failed to open window');
    }
  }).catch(err => {
    // Make sure to reject the promise on failure
    resolver.reject(err);
  });
});

For security reasons, the web pages loaded in the opened window must have valid https certificates and no mixed contents; otherwise the payment request will be aborted by Chrome.

Exchange information between service worker and frontend

The service worker needs to exchange messages with the frontend. To do this, add an event listener.

self.addEventListener('message', e => {

The listener opens a window in order to get authorization from the payer to proceed with payment. To display total price, etc., obtain a message from the opened window that it is ready, then post the payment info back to it. In this case, we set payment_app_window_ready to signal that condition.

  // Determine a message that tells the service worker that
  // the window is ready. In this case `payment_app_window_ready`.
  if (e.data === "payment_app_window_ready") {
    // If `payment_request_event` is not set, this isn't a message
    // preceded by `paymentrequest` event.
    if (!payment_request_event) return;

Now send the payment details back. In this case we're only sending the total of the payment request, but you can pass more details if you like.

    // Query all open windows
    clients.matchAll({
      includeUncontrolled: false,
      type: 'window'
    }).then(clientList => {
      // Send a message that contains information about the payment.
      // In this case it's sending just total.
      for (let client of clientList) {
        client.postMessage(payment_request_event.total);
      }
    });

    return;
  }

Returning payment information

Let's define a message containing https://bobpay.xyz/pay in the methodName as a signal to proceed. Resolve the original paymentrequest event, preserved with the payment details.

  // This app sets `methodName` to be a sign to proceed.
  if (e.data.methodName === methodName) {
    // Resolve with the information passed from frontend.
    // This information will be passed back to the Payment Request
    // a merchant has invoked as a resolution of .show() method.
    resolver.resolve(e.data);
  } else {
    resolver.reject(e.data);
  }
});

Write frontend code

The checkout frontend will be the actual interface that users will interact with. Here's a quick sample.

let client;

navigator.serviceWorker.addEventListener('message', e => {
  client = e.source;
});
navigator.serviceWorker.controller.postMessage('payment_app_window_ready');

onPay() {
  if (!client) return;
  const response = {
    methodName: 'https://payment-handler-example.firebaseapp.com/pay',
    details: { id: '123456' }
  };
  client.postMessage(response);
  // Chrome will close all windows in the scope of the service worker
  // after the service worker responds to the 'paymentrequest' event.
}

onCancel() {
  if (!client) return;
  client.postMessage('The payment request is cancelled by user');
  // Chrome will close all windows in the scope of the service worker
  // after the service worker responds to the 'paymentrequest' event.
}

Again, let's examine this code.

Listen to service worker messages and let it know the window is ready

Let the service worker know that it is ready to receive information by sending a message. Don't forget to listen for its message as well.

// Listen to messages from service worker
navigator.serviceWorker.addEventListener('message', e => {
  // Preserve service worker object for later use
  client = e.source;
  // You can do whatever you want by handling `e.data` as well.
});
// Send `payment_app_window_ready` as a sign that the window is ready.
navigator.serviceWorker.controller.postMessage('payment_app_window_ready');

Invoke the Pay operation

You'll probably want to authenticate the user and let them authorize payment, although that process is outside the scope of this document. Once the user indicates they want to make the payment, send a message to the service worker with the information required for merchant to proceed.

onPay() {
  // When `client` is not found, there's no associated service worker
  if (!client) return;
  // Respond to the service worker with arbitrary message.
  const response = {
    methodName: 'https://payment-handler-example.firebaseapp.com/pay',
    details: { id: '123456' }
  };
  client.postMessage(response);
}

Cancel the operation

You can set up a protocol with the service worker to notify it if the operation is canceled. This code is sending a sample cancel message.

onCancel() {
  if (!client) return;
  client.postMessage('The payment request is cancelled by user');
}

Summary

The Payment Handler API is an emerging web standard that enables a website to act as a payment app by integration into a website that uses the Payment Request API. By implementing these related specifications, you can produce a clean, browser-based UI that makes user purchases as smooth and seamless as a native app.

Feedback

Was this page helpful?