Build digital transactions

You can add dialog to your Action that sells your in-app products in the Google Play store, using the digital purchases API.

This guide walks you through the process of developing an Actions project that incorporates transactions for your digital goods managed through the Google Play store.

Types of digital goods

Your Actions can perform transactions for the following types of digital goods in the Google Play store:

  • One-time purchases only charge the user once. Goods sold as one-time purchases are called "managed products" by the Google Play Console, and can be either “consumed” or “non-consumed” as follows:
    • Non-consumed goods can only be purchased once per user, such as a paid upgrade for an ad-free experience or a game expansion with additional content. By default, one-time purchases are not consumed.
    • Consumed goods can be purchased multiple times, such as a quantity of in-game currency. After the item is purchased, send a consume request to make the item available for purchase again.
  • Subscriptions automatically charge the user on a recurring schedule. These include purchases like a monthly fee for premium features.

Transaction flow

In your Action's conversation, you'll need to perform the following steps to facilitate a digital transaction:

  1. Gather information: Before your Action can handle a transaction, you'll first need to see what items are currently available for the user to purchase.
    1. Check for available Play store SKUs: You query the Play store with the name of a package you own and a list of associated product IDs (SKUs), and it will return a list of which SKUs are available for this user to purchase.
    2. Access the user's existing goods (optional): You can also reference the user's existing entitlements to see what items they've already purchased and tailor their ordering experience.
  2. Build the order: With the list of available SKUs, you present the user an interactive list of purchasable items in the form of a carousel or list rich response.
  3. Complete the purchase: When the user selects an item, you send a purchase request to the Play store containing the SKU of that item and the ID of the user who wishes to purchase it. The user is then prompted to complete the transaction using a pre-defined flow.
  4. Describe the purchase status: After the user completes the transaction flow, you receive a status code. The user sees a response from you that describes whether the transaction was successful or failed.
  5. Make the purchase repeatable (optional): If the user’s purchase was consumable, you send a consume request to the Play store to make the SKU available for future purchase.

Build your project

The following sections explain how to build an Actions project that handles transactions for digital goods.

Setup

Before you can start developing your transactional flow, complete the following setup steps.

Review guidelines

Keep in mind that additional policies apply to Actions with transactions. Please be sure to review our policies and guidelines for transactions.

Actions that sell digital goods can currently only be deployed in the following countries:

  • Australia
  • Brazil
  • Canada
  • Denmark
  • France
  • Indonesia
  • Italy
  • Japan
  • Mexico
  • Netherlands
  • Norway
  • Poland
  • Russia
  • Singapore
  • Spain
  • Sweden
  • Thailand
  • Turkey
  • United Kingdom
  • United States

Play store setup

Since digital transactions are for selling goods that you own in the Google Play store, you'll need to have a Google Play Developer account and a payments profile.

To start selling digital goods in the Google Play store, read the Play console and payments center docs for instructions on registering for a Google Play Developer account and creating a payments profile.

Create a project

When creating your Actions project, you must specify that you want to perform transactions.

To get started, you need to perform the following steps in the Actions console :

  1. Click Add/import project to create a new project or use an existing project.
  2. Navigate to Deploy > Directory information.
  3. Under Additional information > Transactions, check the box that says "Do your Actions use the Digital Purchase API to perform transactions of digital goods?".

Set up a service account for the digital purchases API

To send requests to the digital purchases API, you need to download a JSON service account key associated with your Actions console project, then exchange the service account key for a bearer token that can be passed into the Authorization header of the HTTP request.

To retrieve your service account key, perform the following steps:

  1. Follow this link, swapping "example-project-1" for your project ID: https://console.developers.google.com/apis/api/actions.googleapis.com/overview?project=example-project-1
    • You can find your Actions project ID in the Actions console project settings by clicking the gear icon in the left navigation.
    • If you see an Enable button, click it.
  2. Follow this link, swapping "example-project-1" for your project ID: https://console.developers.google.com/apis/credentials?project=example-project-1
  3. Click Credentials > Create credentials > Service account key.
  4. Under Service Account, click New Service Account.
  5. Give the service account a name like digitaltransactions.
  6. Set Role to Project > Owner.
  7. Select the JSON key type.
  8. Click Create.
  9. A JSON service account key will be downloaded to the local machine.

In your fulfillment, you'll exchange this service key for a bearer token using the Google APIs client library and the https://www.googleapis.com/auth/actions.purchases.digital scope. You can find installation steps and examples on the API client library GitHub page.

Any Android apps whose goods you want users to purchase need to be associated with your Actions project as a connected property.

To connect an app for digital transactions in your Actions project, perform the following steps:

  1. In the Actions console, navigate to Advanced Options > Brand verification.
  2. If you haven't connected any properties, you'll first need to connect a website:
    1. Click the </> button.
    2. Enter the URL for the website you want to connect and click Connect. Google sends you an email confirming the request and an email to the website owner (verified in Search Console) asking them to confirm the association.
  3. Once you have at least one connected website, perform the following steps to connect your Android app:
    1. Click Connect App.
    2. Follow the instructions shown.
    3. Enable Access Play purchases.

1. Gather information

First, your Action gathers information about what items the user can purchase.

1a. Check for available Play store SKUs

User experience

The user expresses an interest in purchasing one of your items in the Play store. For example, your Action asks the user if they'd like to purchase a premium subscription to your app, and they respond "yes".

Create a digital purchases API client

In order to use the digital purchases API, you need to create a JWT client using the https://www.googleapis.com/auth/actions.purchases.digital scope and the service key you generated earlier:

  const serviceAccount = { /* your service account info (load from env) */};
  const request = require('request');
  const {google} = require('googleapis');

  const jwtClient = new google.auth.JWT(
    serviceAccount.client_email, null, serviceAccount.private_key,
    ['https://www.googleapis.com/auth/actions.purchases.digital'],
    null
  );
Request available SKUs

Your Action needs to query the Play store to find out what items are currently available for purchase. You'll provide the package name of an app you want to query, along with the SKUs associated with that package.

You can request the list of available SKUs by sending a POST request to the digital purchases API:

return jwtClient.authorize((err, tokens) => {
    if (err) {
      throw new Error(`Auth error: ${err}`);
    }

    const packageName = 'com.example.projectname';

    request.post(`https://actions.googleapis.com/v3/packages/${packageName}/skus:batchGet`, {
      'auth': {
        'bearer': tokens.access_token,
      },
      'json': true,
      // use the ids that you want to pull info from here.
      'body': {
        'conversationId': conversationId,
        'skuType': 'SKU_TYPE_IN_APP',
        'ids': ['consumable.0', 'consumable.1', 'subscription.monthly.0']
      },
    }, (err, httpResponse, body) => {
      if (err) {
        throw new Error(`API request error: ${err}`);
      }
      console.log(`${httpResponse.statusCode}: ${httpResponse.statusMessage}`);
      console.log(JSON.stringify(body));
    });
  });
});

The response body will contain arrays of SKU objects. Each SKU object has attributes that you'll use to present the item and submit the purchase, as shown in the following code snippet:

body = {
  skus: [
    skuId: {
      skuType: one of "APP", "SUBSCRIPTION", or "UNSPECIFIED"
      id: string,
      packageName: string
    }
    formattedPrice: string,
    title: string,
    description:string,
    subscriptionDetails: {
      subscriptionPeriod: string,
      freeTrialPeriod: string,
      formattedIntroductoryPrice: string
      introductoryPrice: Money
      introductoryPricePeriod: string
    }
  ]
}

1b. Reference the user's existing goods (optional)

When a user sends a request to your webhook, that request's JSON will include a list of purchases they've made for any apps you've linked to this project:

{
  "user": {
    "userId": "xxxx",
    "locale": "en-US",
    "lastSeen": "2018-02-09T01:49:23Z",
    "packageEntitlements": [
      {
        "packageName": "com.abcd.edfg.hijk",
        "entitlements": [
          {
            "sku": "com.abcd.edfg.hijk",
            "skuType": "APP"
          }
        ]
      },
      {
        "packageName": "com.abcd.edfg.lmno",
        "entitlements": [
          {
            "sku": "lmno_jun_2017",
            "skuType": "SUBSCRIPTION",
            "inAppDetails": {
              "inAppPurchaseData": {
                "autoRenewing": true,
                "purchaseState": 0,
                "productId": "lmno_jun_2017",
                "purchaseToken": "12345",
                "developerPayload": "HSUSER_IW82",
                "packageName": "com.abcd.edfg.lmno",
                "orderId": "GPA.233.2.32.3300783",
                "purchaseTime": 1517385876421
              },
              "inAppDataSignature": "V+Q=="
            }
          }
        ]
      }
    ]
  },
  "conversation": {
    "conversationId": "1518141160297",
    "type": "NEW"
  },
  "inputs": [
    {
      "intent": "actions.intent.MAIN",
      "rawInputs": [
        {
          "inputType": "VOICE",
          "query": "Talk to My Test App"
        }
      ]
    }
  ],
  ...
}

In your transactional flow, you could use this information to dynamically exclude items that the user has already purchased, or end the transaction altogether if they've already purchased every available item.

2. Build the order

With the SKUs available for purchase, your Action prompts the user to select an item.

User experience

The user is presented with a list of items they can select for purchase. On a smart speaker, they are read options and prompted to respond. If they are on a screened device, they can tap one of the items.

Build fulfillment

Use the array of SKUs to create a rich response describing the item(s) available for purchase. The code below uses a list response because its contents are simple and can support up to 30 items.

The following code adds the title and description fields of each SKU to a list that will be shown to the user:

skus.forEach((sku) => {
  const key = `${sku.skuId.skuType},${sku.skuId.id}`
  list.items[key] = {
    title: sku.title,
    description: `${sku.description} | ${sku.formattedPrice}`,
  };
});

3. Complete the purchase

Your Action uses the digital purchases API to initiate a purchase with the Play store.

User experience

Once the user has selected an item, the purchase process will be initiated with the Play store. If the conversation isn't happening on the user's phone, they'll be prompted to complete the transaction there. The phone then walks the user through the rest of the transaction (selecting a payment method and purchase confirmation).

Build fulfillment

Call the CompletePurchase() function with details about the SKU that was picked. This function call will handle the rest of the purchase flow for you.

Assuming you used a list, the actions.intent.OPTION event for handling the user's item selection will contain an option parameter describing which SKU was picked:

app.intent('actions.intent.OPTION', (conv, params, option) => {
  let [skuType, id] = option.split(',');

  conv.ask(new CompletePurchase({
    skuId: {
      skuType: skuType,
      id: id,
      packageName: <PACKAGE_NAME>,
    },
  }));
});

4. Describe the purchase status

Once the Play store finishes the user's transaction flow, your Action will receive a status code and notify the user accordingly.

User experience

After the user confirms the purchase on their phone, they receive a response from your Action that lets them know the purchase was successful. Then, your Action ends the conversation.

Fulfillment

When a user completes their purchase, they trigger the actions.intent.COMPLETE_PURCHASE Actions SDK intent, or the actions_intent_COMPLETE_PURCHASE if you're using Dialogflow. This event includes a purchaseStatus field that describes the result of the user's purchase flow. This status isn't automatically sent to the user, so your webhook should handle each status accordingly.

Your fulfillment should handle the following statuses:

  • PURCHASE_STATUS_OK: The purchase was successful. Since the transaction is complete, you should either end the conversation or exit the transactional flow.
  • PURCHASE_STATUS_ALREADY_OWNED: The user already owns that item, so the purchase was unsuccessful. This error status can be avoided by checking the user's existing goods and tailoring the items shown so they don't have the option to re-purchase items they already own.
  • PURCHASE_STATUS_ITEM_UNAVAILABLE: The requested item is not available at the time of the user's purchase. This error status can be avoided by re-checking the available SKUs closer to the time of purchase.
  • PURCHASE_STATUS_ITEM_CHANGE_REQUESTED: The user decided to purchase something else. The best user experience here is to reprompt with your item selection so the user can make another decision right away.
  • PURCHASE_STATUS_USER_CANCELLED: The user cancelled the purchase flow. Since the user prematurely exited the flow, you should ask the user if they want to retry the transaction or exit the transaction altogether.
  • PURCHASE_STATUS_ERROR: The transaction failed for an unknown reason. You should handle this error status by letting the user know the transaction failed, and ask if they want to try again.
  • PURCHASE_STATUS_UNSPECIFIED: The transaction failed for an unknown reason, resulting in an unknown status. You should handle this error status by letting the user know the transaction failed, and ask if they want to try again.

The following code reads purchaseStatus from the arbitrary actions.intent.COMPLETE_PURCHASE intent, which is triggered by the COMPLETE_PURCHASE event, and handles each status accordingly:

// This intent name is arbitrary, the actions_intent_COMPLETE_PURCHASE event
// is what matters
app.intent('actions.intent.COMPLETE_PURCHASE', (conv) => {
  const arg = conv.arguments.get('COMPLETE_PURCHASE_VALUE');
  console.log('User Decision: ' + JSON.stringify(arg));
  if (!arg || !arg.purchaseStatus) {
    conv.close('Purchase failed. Please check logs.');
    return;
  }
  if (arg.purchaseStatus === 'PURCHASE_STATUS_OK') {
    conv.close('Purchase completed! You\'re all set!');
  } else if (arg.purchaseStatus === 'PURCHASE_STATUS_ALREADY_OWNED') {
    conv.close('Purchase failed. You\'ve already owned the item.');
  } else if (arg.purchaseStatus === 'PURCHASE_STATUS_ITEM_UNAVAILABLE') {
    conv.close('Purchase failed. Item is not available.');
  } else if (arg.purchaseStatus === 'PURCHASE_STATUS_ITEM_CHANGE_REQUESTED') {
    // Reprompt with your item selection dialog
  }  } else {
    conv.close('Purchase Failed:' + arg.purchaseStatus);
  }
});

5. Make the purchase repeatable (optional)

By default, a one-time purchase can only be completed once for a given item. If the user purchased a consumable good like a quantity of in-game currency, your Action will send a consume request to the digital purchases API to let the Play store know that this item can be purchased again by this user.

User experience

The user finishes purchasing 100 gold coins for your game, then decides to purchase more. They go through your transaction flow again, and the 100-coin SKU is again presented as a purchase option.

Fulfillment

On a successful purchase, the actions_intent_COMPLETE_PURCHASE event includes a purchase_token field. You can also find this token in the user’s request JSON under the packageEntitlements.

If the SKU should be available for purchase more than once, send a consume request to the digital purchases API with the purchase token. On a successful consume request, the user will be able to purchase that SKU again.

The following code sends a consume request to the digital purchases API and reports whether the request was successful. Keep in mind that you shouldn't show purchase consumption to the user; the following example does this for debug purposes.

request.post(`https://actions.googleapis.com/v3/conversations/${conv.request.conversation.conversationId}/entitlement:consume`, {
  'auth': {
    'bearer': tokens.access_token,
  },
  'json': true,
  'body': {
  // This purchase token is in both the purchase event and the user’s entitlements
  // in their request JSON
    "purchaseToken": entitlement.purchaseToken
  },
  }, (err, httpResponse, body) => {
    if (err) {
     throw new Error(`API request error: ${err}`);
    }
  console.log(`${httpResponse.statusCode}: ${httpResponse.statusMessage}`);
  console.log(JSON.stringify(httpResponse));
  console.log(JSON.stringify(body));
  resolve(body);
});

// Make sure the consume request was successful
return consumePromise.then(body => {
  const consumed = Object.keys(body).length === 0;
  if (consumed) {
    conv.close(`You successfully consumed ${id}`);
  } else {
    conv.close(`Failed to consume: ${id}`);
  }
});