Save passes to Google Pay

All Pass verticals have common use cases. For example, all loyalty cards, gift cards, offers, event tickets, boarding passes for flights, and transit passes can be added to the Google Pay App in multiple ways. Select one of the following methods to learn more:


From an Android app

You can use the following methods to add the Save to Google Pay button to your Android app:

Use the Android SDK

The Android API lets you save Passes in Google Pay. When you integrate the Save to Google Pay button in your app, you make it easy for your customers to save their Pass to Google Pay.

The following steps outline how to add the Save to Google Pay button for the Loyalty Pass, although the process is the same for all Passes.

1. Create a class

First, define the LoyaltyClass. The following example shows a JSON resource that represents a LoyaltyClass:

{
  "accountIdLabel": "Member Id",
  "accountNameLabel": "Member Name",
  "id": "2945482443380251551.ExampleClass1",
  "issuerName": "Baconrista",
  "kind": "walletobjects#loyaltyClass",
  "textModulesData": [
    {
      "header": "Rewards details",
      "body": "Welcome to Baconrista rewards.  Enjoy your rewards for being a loyal customer. " +
               "10 points for every dollar spent.  Redeem your points for free coffee, bacon and more!"
    }
  ],
  "linksModuleData": {
    "uris": [
      {
        "kind": "walletobjects#uri",
        "uri": "https://maps.google.com/map?q=google",
        "description": "Nearby Locations"
      },
      {
        "kind": "walletobjects#uri",
        "uri": "tel:6505555555",
        "description": "Call Customer Service"
      }
    ]
  },
  "imageModulesData": [
    {
      "mainImage": {
        "kind": "walletobjects#image",
        "sourceUri": {
          "kind": "walletobjects#uri",
          "uri": "https://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg",
          "description": "Coffee beans"
        }
      }
    }
  ],
  "messages": [{
    "header": "Welcome to Banconrista Rewards!",
    "body": "Featuring our new bacon donuts.",
    "kind": "walletobjects#walletObjectMessage"
  }],
  "locations": [{
    "kind": "walletobjects#latLongPoint",
    "latitude": 37.424015499999996,
    "longitude": -122.09259560000001
    },{
    "kind": "walletobjects#latLongPoint",
    "latitude": 37.424354,
    "longitude": -122.09508869999999
    },{
    "kind": "walletobjects#latLongPoint",
    "latitude": 37.7901435,
    "longitude": -122.39026709999997
    },{
    "kind": "walletobjects#latLongPoint",
    "latitude": 40.7406578,
    "longitude": -74.00208940000002
  }],
  "programLogo": {
    "kind": "walletobjects#image",
    "sourceUri": {
      "kind": "walletobjects#uri",
      "uri": "https://farm8.staticflickr.com/7340/11177041185_a61a7f2139_o.jpg"
    }
  },
  "programName": "Baconrista Rewards",
  "rewardsTier": "Gold",
  "rewardsTierLabel": "Tier",
  "reviewStatus": "underReview",
  "hexBackgroundColor": "#ffffff",
  "heroImage": {
   "kind": "walletobjects#image",
   "sourceUri": {
     "kind": "walletobjects#uri",
     "uri": "https://farm8.staticflickr.com/7302/11177240353_115daa5729_o.jpg"
   }
  }
}

2. Create an object

After you create the class, define the LoyaltyObject, as shown in the following snippet:

{
  "classId": "2945482443380251551.ExampleClass1",
  "id": "2945482443380251551.ExampleObject1",
  "accountId": "1234567890",
  "accountName": "Jane Doe",
  "barcode": {
    "alternateText": "12345",
    "type": "qrCode",
    "value": "28343E3"
  },
  "textModulesData": [{
    "header": "Jane's Baconrista Rewards",
    "body": "Save more at your local Mountain View store Jane. " +
              "You get 1 bacon fat latte for every 5 coffees purchased.  " +
              "Also just for you, 10% off all pastries in the Mountain View store."
  }],
  "linksModuleData": {
    "uris": [
      {
        "kind": "walletobjects#uri",
        "uri": "https://www.baconrista.com/myaccount?id=1234567890",
        "description": "My Baconrista Account"
      }]
  },
  "infoModuleData": {
    "labelValueRows": [{
      "columns": [{
        "label": "Next Reward in",
        "value": "2 coffees"
      }, {
        "label": "Member Since",
        "value": "01/15/2013"
      }]
    }, {
      "columns": [{
        "label": "Local Store",
        "value": "Mountain View"
      }]
    }],
    "showLastUpdateTime": "true"
  },
  "loyaltyPoints": {
    "balance": {
      "string": "5000"
    },
    "label": "Points",
      "pointsType": "points"
  },
  "messages": [{
    "header": "Jane, welcome to Banconrista Rewards!",
    "body": "Thanks for joining our program. Show this message to " +
              "our barista for your first free coffee on us!"
  }],
  "state": "active"
}

3. Encode an unsigned JWT

After you create the object, encode the LoyaltyClass and LoyaltyObject into an unsigned JWT, as shown in the following snippet:

{
  "iss": "example_service_account@developer.gserviceaccount.com",
  "aud": "google",
  "typ": "savetoandroidpay",
  "iat": 1368029586,
  "payload": {
    "eventTicketClasses": [{
      ... //Event ticket Class JSON
    }],
    "eventTicketObjects": [{
      ... //Event ticket Object JSON
    }],
    "flightClasses": [{
      ... //Flight Class JSON
    }],
    "flightObjects": [{
      ... //Flight Object JSON
    }],
    "giftCardClasses": [{
      ... //Gift card Class JSON
    }],
    "giftCardObjects": [{
      ... //Gift card Object JSON
    }],
    "loyaltyClasses": [{
      ... //Loyalty Class JSON
    }],
    "loyaltyObjects": [{
      ... //Loyalty Object JSON
    }],
    "offerClasses": [{
      ... //Offer Class JSON
    }],
    "offerObjects": [{
      ... //Offer Object JSON
    }],
    "transitClasses": [{
      ... //Transit Class JSON
    }],
    "transitObjects": [{
      ... //Transit Object JSON
    }]
  },
  "origins": ["http://baconrista.com", "https://baconrista.com"]
}

4. Choose a request format to use

The Android SDK lets you make requests with one of the following formats:

For more details on how to make a request, see Call the Android SDK.

savePasses

Requests to the savePasses method have a JSON string payload. This means you can directly use the JSON for the object created in step 3.

You can make requests to Save to Google Pay for classes and objects that already exist or that are inserted as part of the save process. You can also save multiple passes in the same request if the pass vertical supports that functionality. You can't upsert classes and objects that already exist.

In order to improve security, some fields are considered sensitive, and in these cases you can't save passes that already exist by just specifying the object ID field. You can only save objects that already exist if the sensitive fields in the request match the fields of the objects that already exist. The following fields are considered sensitive:

  • object.barcode.value
  • object.smartTapRedemptionValue
savePassesJwt

Requests to the savePassesJwt method have a JWT string token payload. To create the JWT, sign the object from step 3 with your OAuth 2.0 service account private key. The following snippets show how to encode a JWT in various languages:

Java

WobCredentials credentials = null;
WobUtils utils = null;

// Instantiate the WobUtils class which contains handy functions
// Wob utils can be found in the quickstart sample
try {
  credentials = new WobCredentials(
    ServiceAccountEmailAddress,
    ServiceAccountPrivateKeyPath,
    ApplicationName,
    IssuerId);
  utils = new WobUtils(credentials);
} catch (GeneralSecurityException e) {
  e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
}

// Add valid domains for the Save to Wallet button
List<String> origins = new ArrayList<String>();
origins.add("http://baconrista.com");
origins.add("https://baconrista.com");
origins.add(req.getScheme() + "://" + req.getServerName() + ":" + req.getLocalPort());

//Generate Objects and Classes here
//........

WobPayload payload = new WobPayload();
payload.addObject({WalletObject/WalletClass});

// Convert the object into a Save to Android Pay Jwt
String jwt = null;
try {
  jwt = utils.generateSaveJwt(payload, origins);
} catch (SignatureException e) {
  e.printStackTrace();
}

PHP

$requestBody = [
  "iss"=> SERVICE_ACCOUNT_EMAIL_ADDRESS,
  "aud" => "google",
  "typ" => "savetoandroidpay",
  "iat"=> time(),
  "payload" => {
    "eventTicketClasses" => [ ], # Event ticket classes
    "eventTicketObjects" => [ ], # Event ticket objects
    "flightClasses" => [ ],      # Flight classes
    "flightObjects" => [ ],      # Flight objects
    "giftCardClasses" => [ ],    # Gift card classes
    "giftCardObjects" => [ ],    # Gift card objects
    "loyaltyClasses" => [ ],     # Loyalty classes
    "loyaltyObjects" => [ ],     # Loyalty objects
    "offerClasses" => [ ],       # Offer classes
    "offerObjects" => [ ],       # Offer objects
    "transitClasses" => [ ],     # Transit classes
    "transitObjects" => [ ]      # Transit objects
  },
  "origins" => ["http://baconrista.com", "https://baconrista.com"]
]
// Generate the Save to Android Pay Jwt
echo $jwt = $assertObj->makeSignedJwt($requestBody, $client);

Python

jwt = {
  'iss': config.SERVICE_ACCOUNT_EMAIL_ADDRESS,
  'aud': 'google',
  'typ': 'savetoandroidpay',
  'iat':  int(time.time()),
  'payload': {
    'webserviceResponse': {
      'result': 'approved',
      'message': 'Success.'
    },
    'eventTicketClasses': [], # Event ticket classes
    'eventTicketObjects': [], # Event ticket objects
    'flightClasses': [],      # Flight classes
    'flightObjects': [],      # Flight objects
    'giftCardClasses': [],    # Gift card classes
    'giftCardObjects': [],    # Gift card objects
    'loyaltyClasses': [],     # Loyalty classes
    'loyaltyObjects': [],     # Loyalty objects
    'offerClasses': [],       # Offer classes
    'offerObjects': [],       # Offer objects
    'transitClasses': [],     # Transit classes
    'transitObjects': []      # Transit objects
  },
  'origins' : ['http://baconrista.com', 'https://baconrista.com']
}

// Generate the Save to Android Pay Jwt
signer = crypt.Signer.from_string(app_key)
signed_jwt = crypt.make_signed_jwt(signer, jwt)
response = webapp2.Response(signed_jwt)

You can make requests to Save to Google Pay for classes and objects that already exist or that are inserted as part of the save process. You can also save multiple passes in the same request if the pass vertical supports that functionality. You can't upsert classes and objects that already exist. Skinny JWTs can be used as long as the classes and objects already exist.

5. Call the Android SDK

First, use the getPayApiAvailabilityStatus method to check whether the savePasses or savePassesJwt methods are available, as shown in the following example:

import com.google.android.gms.common.api.UnsupportedApiCallException;
import com.google.android.gms.pay.Pay;
import com.google.android.gms.pay.PayApiAvailabilityStatus;
import com.google.android.gms.pay.PayClient;
…
PayClient payClient = Pay.getClient(this);
payClient
  // Use PayClient.RequestType.SAVE_PASSES_JWT for the savePassesJwt API
  .getPayApiAvailabilityStatus(PayClient.RequestType.SAVE_PASSES)
  .addOnSuccessListener(
    status -> {
      switch (status) {
        case PayApiAvailabilityStatus.AVAILABLE:
          // You can call the savePasses API or savePassesJwt API
          ...
          break;
        case PayApiAvailabilityStatus.NOT_ELIGIBLE:
        default:
          // We recommend to either:
          // 1) Hide the save button
          // 2) Fall back to a different Save Passes integration (e.g. JWT link)
          //    Note however that the user *will* only be able to access their
          //    passes on web
          // A not eligible user might become eligible in the future.
          ...
          break;
        }
      })
  .addOnFailureListener(
    exception -> {
      if (exception instanceof UnsupportedApiCallException) {
        // Google Play Services too old. We could not check API availability or
        // user eligibility. We recommend to either:
        // 1) Fall back to a different Save Passes integration (e.g. JWT link)
        //    Note however that the user *may* only be able to access their
        //    passes on web
        // 2) Hide the save button
        ...
      } else {
        // Very old version of Google Play Services or unexpected error!
        ...
      }
    });

If the API is available, call the savePasses or savePassesJwt method when the user taps on the Save to Google Pay button.

savePasses

private static final int SAVE_TO_GOOGLE_PAY = 1000;
…
String jsonString = … // Build or fetch JSON request
PayClient payClient = Pay.getClient(this);
payClient.savePasses(jsonString, this, SAVE_TO_GOOGLE_PAY);

savePassesJwt

private static final int SAVE_TO_GOOGLE_PAY = 1000;
…
String jwtString = … // Fetch JWT from a secure server
PayClient payClient = Pay.getClient(this);
payClient.savePassesJwt(jwtString, this, SAVE_TO_GOOGLE_PAY);

This call triggers the save flow. After the flow finishes, your app resolves the result with onActivityResult. In your Activity, this receiver needs to be defined similar to the following:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
  // `data` will only have information in the `SAVE_ERROR` case
  if (requestCode == SAVE_TO_GOOGLE_PAY) {
    switch (resultCode) {
      case Activity.RESULT_OK:
        // Save successful
        ...
        break;
      case Activity.RESULT_CANCELED:
        // Save canceled
        ...
        break;
      case PayClient.SavePassesResult.API_ERROR:
        // API error - this should not happen if getPayApiAvailabilityStatus is
        // used correctly
        ...
        break;
      case PayClient.SavePassesResult.SAVE_ERROR:
        // Save error - check EXTRA_API_ERROR_MESSAGE to debug the issue
        // Most save errors indicate an error in the app fingerprint or the Json
        // request payload. In most cases prompting the user to try again will not
        // help.
        if (data != null &&
            !isEmpty(data.getStringExtra(PayClient.EXTRA_API_ERROR_MESSAGE))) {
          ...
        } else {
          // Unexpected! A save error should always have a debug message associated
          // with it
          ...
        }
        break;
      case PayClient.SavePassesResult.INTERNAL_ERROR:
      default:
        // Internal error - prompt the user to try again, if the error persists
        // disable the button
        ...
        break;
    }
  } else {
    ...
  }
}

6. Add the Save to Google Pay button to your UI

Google Pay provides an Android SDK button for you to integrate into your app. The button assets are available in the Brand guidelines.

This toolkit contains vector images of the buttons.

To incorporate a button into your application, copy over the button image from the toolkit into the res folder of your application and add the following code to your Android layout file. Note that each button requires its unique contentDescription string and minWidth value in addition to the correct value for src.

<ImageButton
             android:layout_width="match_parent"
             android:layout_height="48dp"
             android:minWidth="200dp"
             android:clickable="true"
             android:src="@drawable/s2ap" />

The layout_height for the button is 48 dp and minWidth must be 200 dp.

Use the following steps to save your Pass to Google Pay from your app:

  1. Complete the steps in Add the Save to Google Pay button to your email or SMS.
  2. Use an ACTION_VIEW intent to open the deep link from the Save to Google Pay button.

    Make sure that the button that triggers the intent uses the Brand guidelines.

The following is an example flow summary:

  1. At some time before a Pass is saved, a class is created on the backend with the REST API.
  2. When the end user asks to saves a pass, your server backend sends a JWT to your Android client app that represents an object.
  3. Your Android client app includes a Save to Google Pay button that follows our brand guidelines. When clicked, it opens an ACTION_VIEW intent to a URI that includes the JWT in its path. Here's an example:
    https://pay.google.com/gp/v/save/{jwt_generated}
    

Use the JWT POST request method

The JWT POST request method is an alternative method to create flight or event ticket classes and objects for Android apps. It's used when it's difficult to implement the backend work required to create and insert a class before an object is saved. This method is most useful for event tickets and boarding passes, which are Passes that can have many classes created over time. Its flow is summarized as follows:

  1. When the end user checks in to their flight, or redeems an event ticket, your server backend renders a JWT to your Android client app that includes both the class and object.
  2. Your Android client app includes a Save to Google button that follows our brand guidelines. When you click the button, the following happens:
    1. A POST request sends the JWT to a Google endpoint through HTTPS.
    2. A URI from the resulting HTTP response body is sent in return, which should then be used to open an ACTION_VIEW intent.

The JWT POST request method also requires an API key. This is appended as a query parameter to the REST API call.

Class creation

A new class is only created on our backend when presented with a class.id that hasn't been saved in the past. Therefore, while you might potentially pass the class details to Google through the JWT multiple times, the backend recognizes that the class is already saved, and it doesn't create new ones every time a boarding pass is saved.

Class updates

After the first boarding pass, the object is saved along with the class. You can use the class.id as expected with our REST API to perform ADDMESSAGE, GET, LIST, PATCH, and UPDATE operations as expected.

To change class details, you must use the Class Update API. If you create a class with class.id=XYZ and some other class details, and you attempt to create a class later with class.id=XYZ but with different class details, we still maintain the original class and won't apply any changes.

JWT format

The format for the JWT you send is described in detail by our reference documentation about Google Pay API for Passes JWT. For this payload, you pass one entry for objects, which represents the object you want to create, and one entry for classes, which contains the class that you created.

HTTP request

You can use the INSERT method to insert classes and objects specified in a JWT. The API Key must be set as a query parameter.

JWT INSERT method

Given a JWT, the INSERT method inserts the classes and objects specified in a JWT. If successful, it returns a 200 HTTP response.

HTTP request
POST https://walletobjects.googleapis.com/walletobjects/v1/jwt/

Authorization

This request requires no authorization. However, the JWT must be signed with RSA-SHA256. The signing key is the OAuth service account-generated key.

Request body

In the request body, supply data with the following structure:

{ “jwt” : string }

Response body

When successful, this method returns a response body with the following structure:

{
    "saveUri": string,
    "resources": {
      "eventTicketClasses": [ eventTicketClass resource, ... ],
      "eventTicketObjects": [ eventTicketObject resource, ... ],
      "flightClasses": [ flightClass resource, ... ],
      "flightObjects": [ flightObject resource, ... ],
      "giftCardClasses": [ giftCardClass resource, ... ],
      "giftCardObjects": [ giftCardObject resource, ... ],
      "loyaltyClasses": [ loyaltyClass resource, ... ],
      "loyaltyObjects": [ loyaltyObject resource, ... ],
      "offerClasses": [ offerClass resource, ... ],
      "offerObjects": [ offerObject resource, ... ],
      "transitClasses": [ transitClass resource, ... ],
      "transitObjects": [ transitObject resource, ... ]
    }
}

A saveUri is a URI that, when opened, allows the end user to save the objects identified in the JWT to their Google account. This URI is only valid a week after it's returned.

See the JWT endpoint reference for more details.

Flow diagrams

See Typical API flows for flow diagrams.