Building an App with Transactions

Building transactions involves the following steps:

Read the prerequisites
See the Before you start section for more information on transaction requirements and guidelines.
Create an Actions project with transactions enabled

When creating your app, you must specify that you want to use the Transactions API. This lets us enable certain features to enable transactions for your app and also lets us know to review specific transactions features when you submit your app for approval.

See Create an Actions project for more information.

Build the order

Transactions typically fall under two use cases. New orders, where you let users to build a "shopping cart" and execute the transaction or reorders, where you can quickly use a previous transaction and execute a new one based on that. Reorders require some sort of user identity to track user transactions over time.

To build the order, check that the user can transact and then build the order. You can use the delivery address helper to get a user's delivery address if required.

Propose the order

Once you have the ProposedOrder built, confirm the transaction with the user and carry out the transaction, using a payment method if required.

See perform account linking for information on linking accounts if applicable and then propose the order to the user.

Confirm the order

Once the user confirms the transaction, carry it out and confirm the order to the user with a receipt.

See confirm the order and send a receipt for more information.

Send updates
After a transaction is executed, you must provide updates throughout the life of the order until the order is completed (for example, delivered or cancelled). An order's state should always reflect the state that the user sees on your website or app.

See send order updates for more information.

Before you start

Ensure you go through the following sections before starting.

Read policies and guidelines

See the policies and guidelines document for information on how to distribute your apps to users.

Choose a payment method

Before building your app, you should decide what payment methods it will use. In general, there are three major categories.

  • Use your own payment method - if you have your own web or mobile app, you can allow users to set up and store payment methods there. If your identity store is OAuth 2.0-enabled, you can configure account linking settings in the Actions Console and use the actions.intent.SIGN_IN intent to sign in the user, and call your own API to charge their payment method when they create transactions.

  • Use a Google-provided payment method - users can now set up payment methods in their Google Assistant settings to make payments during interactions with the Assistant and your Assistant apps. If you have an account on Stripe, Braintree, or Vantiv, you can request payment via a user's Google-provided payment method, and we will send you a chargeable payment token for use with your payment processor.

  • No payment method - for some use cases (primarily reservations), there may be no payment associated with the transaction. In this case, you may create a transaction with no payment method specified.

Learn about the transactions intents

When building transactions into your app, you can request the following intents to be fulfilled throughout your conversation to help you complete the transaction:

  • actions.intent.TRANSACTION_REQUIREMENTS_CHECK - Ensures that a user can actually participate in transactions. This is useful for checking that a user doesn't go through creating an order and later finding out they can't transact. This is a silent intent, meaning it doesn't have a conversation with the user when triggered.
  • actions.intent.DELIVERY_ADDRESS - Obtains the user's delivery address. This is useful for things like presenting nearby locations at the beginning of a conversation or for obtaining an address towards the end of the transaction process.
  • actions.intent.TRANSACTION_DECISION - Confirms the transaction with the user. This intent silently triggers actions.intent.TRANSACTION_REQUIREMENTS_CHECK to ensure the user can participate in transactions. Requesting this intent is required to complete a transaction. You will pass payment method configuration details to Google along with this intent.
  • actions.intent.SIGN_IN - Asks the user to sign into their account with your service and link it to their Google account. Users can even sign up for a new account with your service and link it to their existing account with this flow. This is useful for asking users to sign up for an account on your service after a successful transaction or linking their existing account before a transaction occurs to obtain payment methods.

The following sections show how to use these intents with the Node.js client library (highly recommended) or by constructing JSON payloads for the conversation webhook.

Building an app with transactions

Create an Actions project

To get started, you will need to perform the following steps the Actions Console:

  • Create a new project or import an existing project
  • Completely fill out the "App Information" section
  • Under "Privacy and Consent", you must check the box that says "Does your app perform Transactions?". Otherwise, the Transactions APIs will fail when you attempt to use them.

We recommend using API.AI to build your app. It will make it much easier to build a high-quality conversational ordering experience.

For an example of a complete transactional app, download or clone the transactions sample on GitHub.

Check for transaction requirements

User experience

As soon as the user has indicated they wish to transact, you should use the actions.intent.TRANSACTION_REQUIREMENTS_CHECK intent to ensure they will be able to perform a transaction. For example, when invoked your app might ask "would you like to order shoes, or check your account balance?" If the user says "order shoes", you should request this intent right away, which will make sure they will be able to proceed, and give them an opportunity to fix any settings preventing them from continuing with the transaction.

Once you request the intent, the Assistant will check hard requirements like the user's locale, device type etc. If these are met, it will check requirements that are fixable like the user's payment settings. The Assistant may guide the user through fixing their settings so they can fulfill the requirements.

Once the requirements are met, the intent will be sent back to your app with a success condition and you can proceed with building the user's order.

If one or more of the requirements cannot be met, the intent will be sent back to your app with a failure condition. In this case, you should pivot the conversation away from the transactional experience, or end the conversation.

API

To ensure that a user meets transaction requirements, request fulfillment of the actions.intent.TRANSACTION_REQUIREMENTS_CHECK intent with a TransactionRequirementsCheckSpec data object, which defines the following properties:

  • orderOptions - customer information your app requires for the transactions
    • requestDeliveryAddress - if true, delivery address is required for the order
    • customerInfoOptions.customerInfoProperties[] - array of CustomerInfoProperty enum. Currently, the only valid value is "EMAIL"
  • paymentOptions - only needed if using a Google-provided payment method.
    • googleProvidedOptions.supportedCardNetworks[] - valid card networks for the user to use to pay. Array of CardNetwork enum, which has valid values: "AMEX", "DISCOVER", "MASTERCARD", "VISA", "JCB"
    • googleProvidedOptions.prepaidCardDisallowed - boolean indicating whether the user may pay with a prepaid card on file with Google
Checking requirements with a Google payment method

You can check to see if a user satisfies transactions requirements for a Google-provided payment
method in the following way.

API.AI Node.js
function transactionCheckGooglePayment (app) {
    app.askForTransactionRequirements({
      // These will be provided by payment processor, like Stripe, Braintree, or
      // Vantiv
      tokenizationParameters: {},
      cardNetworks: [
        app.Transactions.CardNetwork.VISA,
        app.Transactions.CardNetwork.AMEX
      ],
      prepaidCardDisallowed: false,
      deliveryAddressRequired: false
    });
  }
    
API.AI JSON
{
  "google": {
    "systemIntent": {
      "intent": "actions.intent.TRANSACTION_REQUIREMENTS_CHECK",
      "data": {
        "@type": "type.googleapis.com/google.actions.v2.TransactionRequirementsCheckSpec",
        "paymentOptions": {
          "googleProvidedOptions": {
            "prepaidCardDisallowed": false,
            "supportedCardNetworks": [
              "VISA", "AMEX"
            ]
          }
        },
        "orderOptions": {
          "requestDeliveryAddress": true
        }
      }
    }
  }
}
Checking requirements with your own payment method

You can check to see if a user satisfies transactions requirements when using your own payment method in the following way.

API.AI Node.js
function transactionCheckActionPayment (app) {
    app.askForTransactionRequirements({
      type: app.Transactions.PaymentType.PAYMENT_CARD,
      displayName: 'VISA-1234',
      deliveryAddressRequired: false
    });
  }
    
API.AI JSON
{
  "google": {
    "systemIntent": {
      "intent": "actions.intent.TRANSACTION_REQUIREMENTS_CHECK",
      "data": {
        "@type": "type.googleapis.com/google.actions.v2.TransactionRequirementsCheckSpec",
        "paymentOptions": {
          "actionProvidedOptions": {
            "paymentType": "PAYMENT_CARD",
            "displayName": "VISA-9876"
          }
        },
        "orderOptions": {
          "requestDeliveryAddress": true
        }
      }
    }
  }
}
Receiving the result of a requirements check

After the Assistant fulfills the intent, it sends your fulfillment a request with the actions.intent.TRANSACTION_REQUIREMENTS_CHECK intent with the result of the check.

To properly handle this request, declare an API.AI intent that's triggered by the actions_intent_TRANSACTION_REQUIREMENTS_CHECK event. When triggered, handle it in your fulfillment in the following way.

API.AI Node.js
function transactionCheckComplete (app) {
    if (app.getTransactionRequirementsResult() === app.Transactions.ResultType.OK) {
      // Normally take the user through cart building flow
      app.ask('Looks like you\'re good to go! Try saying "Get Delivery Address".');
    } else {
      app.tell('Transaction failed.');
    }
  }
    

Obtain a delivery address

If your transaction requires a user's delivery address, you can request fulfillment of the actions.intent.DELIVERY_ADDRESS intent. This might be useful for determining the total price, delivery/pickup location, or for ensuring the user is within your service region.

When requesting this intent to be fulfilled, you pass in a reason option that allows you preface the Assistant's request to obtain an address with a string. For example, if you specify "to know where to send the order", the Assistant might ask the user:

"To know where to send the order, I'll need to get your delivery address"

Requesting the delivery address

API.AI Node.js
  function deliveryAddress (app) {
    app.askForDeliveryAddress('To know where to send the order');
  }
API.AI JSON
{
  "google": {
    "systemIntent": {
      "intent": "actions.intent.DELIVERY_ADDRESS",
      "data": {
        "@type": "type.googleapis.com/google.actions.v2.DeliveryAddressValueSpec",
        "addressOptions": {
          "reason": "To proceed with your order"
        }
      }
    }
  }
}
    

Receiving the delivery address

After the Assistant fulfills the intent, it sends your fulfillment a request with the actions.intent.DELIVERY_ADDRESS intent.

To properly handle this request, declare an API.AI intent that's triggered by the actions_intent_DELIVERY_ADDRESS event. When triggered, handle it in your fulfillment in the following way.

API.AI Node.js
function deliveryAddressComplete (app) {
    if (app.getDeliveryAddress()) {
      console.log('DELIVERY ADDRESS: '
        + app.getDeliveryAddress().postalAddress.addressLines[0]);
      app.ask('Great, got your address! Now say "confirm transaction".');
    } else {
      app.tell('Transaction failed.');
    }
  }

Build an order

User experience

Once you have the user information you need, you'll want to build a "cart assembly" experience that guides the user to build an order. Every app will likely have a slightly different cart assembly flow as appropriate for your product or service.

A great cart assembly experience could be as simple as enabling the user to re-order their most recent purchase via a simple yes or no question. You may want to present the user a carousel or list card of the top "featured" or "recommended" items. We highly recommend using rich responses and presenting the user's options in a visual way.

For more information on how to build a great cart assembly experience, see the Transactions Design Guidelines.

API

Throughout your conversation, you'll need to gather the items that a user wants to add to their order and then construct a ProposedOrder that consists of:

  • merchant - identifies you by ID and name.
  • lineItems - collection of items in the order cart
  • totalPrice - the price of all items in the cart
  • otherItems - tax and subtotal that can be displayed on the user's receipt when the order is completed.
  • extension.locations - the locations (e.g. pickup, delivery, etc.) associated with the order

You will also want to collect other information that will be "proposed" to the user as part of the transaction request, such as their preferred payment method and delivery location.

See the conversation webhook documentation and Node.js client library reference for more information.

Building an order
API.AI Node.js
let order = app.buildOrder('<UNIQUE_ORDER_ID>')
  .setCart(app.buildCart().setMerchant('book_store_1', 'Book Store')
    .addLineItems([
      app.buildLineItem('memoirs_1', 'My Memoirs')
        .setPrice(app.Transactions.PriceType.ACTUAL, 'USD', 3, 990000000)
        .setQuantity(1)
        .addSublines('Note from the author'),
      app.buildLineItem('memoirs_2', 'Memoirs of a person')
        .setPrice(app.Transactions.PriceType.ACTUAL, 'USD', 5, 990000000)
        .setQuantity(1)
        .addSublines(['Special introduction by author', 'Something else from the author']),
      app.buildLineItem('memoirs_3', 'Their memoirs')
        .setPrice(app.Transactions.PriceType.ACTUAL, 'USD', 15, 750000000)
        .setQuantity(1)
        .addSublines(
          app.buildLineItem('memoirs_epilogue', 'Special memoir epilogue')
            .setPrice(app.Transactions.PriceType.ACTUAL, 'USD', 3, 990000000)
            .setQuantity(1)
        ),
      app.buildLineItem('memoirs_4', 'Our memoirs')
        .setPrice(app.Transactions.PriceType.ACTUAL, 'USD', 6, 490000000)
        .setQuantity(1)
    ]).setNotes('The Memoir collection'))
  .addOtherItems([
    app.buildLineItem('subtotal', 'Subtotal')
      .setType(app.Transactions.ItemType.SUBTOTAL)
      .setQuantity(1)
      .setPrice(app.Transactions.PriceType.ESTIMATE, 'USD', 32, 220000000),
    app.buildLineItem('tax', 'Tax')
      .setType(app.Transactions.ItemType.TAX)
      .setQuantity(1)
      .setPrice(app.Transactions.PriceType.ESTIMATE, 'USD', 2, 780000000)
  ])
  .setTotalPrice(app.Transactions.PriceType.ESTIMATE, 'USD', 38, 990000000);
  

Perform account linking

When using your own payment method to charge the user, you will likely need to link their Google account with an account they have with your own service to retrieve, present, and charge payment methods stored there.

We provide OAuth 2.0 account linking to satisfy this requirement. You can find more information on account linking in general here. We highly recommend enabling the OAuth 2.0 Assertion flow as it enables a very streamlined user experience.

We provide the actions.intent.SIGN_IN intent which allows you to request that a user link accounts mid-conversation.

You should call this API if you are unable to find an accessToken in the User object in the webhook request. This means that the user has not yet linked their account.

After requesting the actions.intent.SIGN_IN intent, you will receive an Argument containing a SignInStatus with a value of either "OK", "CANCELLED", or "ERROR". If the status is "OK" you should be able to fnd an accessToken in the User object.

Note that you must set up account linking in the Actions Console to use the actions.intent.SIGN_IN intent.

API

Requesting signin
API.AI Node.js
function signIn (app) {
  app.askForSignIn()
}
API.AI JSON
{
  "google": {
    "systemIntent": {
      "intent": "actions.intent.SIGN_IN",
      "data": {
        "@type": "type.googleapis.com/google.actions.v2.SignInValueSpec"
      }
    }
  }
}
    
Receiving the sign in result
API.AI Node.js
if (app.getSignInStatus() == app.SignInStatus.OK) {
    let accessToken = app.getUser().accessToken;
    // use the accessToken to access your API
}

Propose the order to the user

Once you've built an order, you must present it to the user to confirm or reject. You do this by requesting the actions.intent.TRANSACTION_DECISION intent and providing the order that you built.

User Experience

When you request the actions.intent.TRANSACTION_DECISION intent, the Assistant initiates a built-in experience in which the ProposedOrder you passed is rendered directly onto a "cart preview card". The user can say "place order", decline the transaction, or change a payment option like the credit card or address.

The user might also say something that is not matched by our built-in experience like "change my latte to a triple". In this case, the query will be sent to your app like a normal query. You should set up your API.AI intents to be ready for order change requests such as this.

API

You must provide a TransactionDecisionValueSpec when you request the actions.intent.TRANSACTION_DECISION intent. This contains the ProposedOrder as well as your OrderOptions and PaymentOptions.

After the user accepts or rejects the transaction, you will receive an Argument containing a TransactionDecisionValue. This will contain:

  • transactionRequirementsCheckResult.resultType - before presenting the cart preview card to the user, the Assistant will automatically perform the same logic contained in the actions.intent.TRANSACTION_REQUIREMENTS_CHECK intent. The result of this is contained in the resultType field. Possible values are "OK", "USER_ACTION_REQUIRED", "ASSISTANT_SURFACE_NOT_SUPPORTED" and "REGION_NOT_SUPPORTED"
  • userDecision - the user's decision regarding the proposed order. Possible values are "ORDER_ACCEPTED", "ORDER_REJECTED", "DELIVERY_ADDRESS_UPDATED", and "CART_CHANGE_REQUESTED"
  • deliveryAddress - if the user changed the delivery address, the updated address. Note that userDecision will be "DELIVERY_ADDRESS_UPDATED" in this case.

Assuming userDecision was "ORDER_ACCEPTED":

  • order.finalOrder - a copy of the ProposedOrder that was originally passed
  • order.googleOrderId - a Google-generated order ID that can be used to reference the order later
  • order.orderDate - the date and time the order was created
  • order.paymentInfo - the details regarding the payment method that must be used to charge the user
    • paymentType - one of "PAYMENT_CARD", "BANK', "LOYALTY_PROGRAM", "ON_FULFILLMENT", "GIFT_CARD" Returned for all types of payment methods
    • googleProvidedPaymentInstrument.instrumentToken - contains a Base64-encoded payment token provided by a third-party payment processor
      Returned for Google-provided payment methods only
    • displayName - name of the instrument displayed on the receipt
      Returned for payment methods provided by your app only
  • order.customerInfo - any customer information (e.g. email address) requested
Use a Google payment method
API.AI Node.js
      app.askForTransactionDecision(order, {
          // These will be provided by payment processor, like Stripe,
          // Braintree, or Vantiv
          tokenizationParameters: {
            "gateway": "stripe",
            "stripe:publishableKey": (app.isInSandbox() ? "pk_test_key" : "pk_live_key"),
            "stripe:version": "2017-04-06"
          },
          cardNetworks: [
            app.Transactions.CardNetwork.VISA,
            app.Transactions.CardNetwork.AMEX
          ],
          prepaidCardDisallowed: false,
          deliveryAddressRequired: false
      });
  
API.AI JSON
{
  "google": {
    "systemIntent": {
      "intent": "actions.intent.TRANSACTION_DECISION",
      "data": {
        "@type": "type.googleapis.com/google.actions.v2.TransactionDecisionValueSpec",
        "proposedOrder": ...,
        "paymentOptions": {
          "googleProvidedOptions": {
            "tokenizationParameters": {
              "tokenizationType": "PAYMENT_GATEWAY",
              "parameters": {
                "gateway": "stripe",
                "stripe:publishableKey": "pk_test_key",
                "stripe:version": "2017-04-06"
              }
            }
          }
        },
        "orderOptions": {
          "customerInfoOptions": {
            "customerInfoProperties": [
              "EMAIL"
            ]
          }
        }
      }
    }
  }
}
Provide your own payment method
API.AI Node.js
     app.askForTransactionDecision(order, {
        type: app.Transactions.PaymentType.PAYMENT_CARD,
        displayName: 'VISA-1234',
        deliveryAddressRequired: true
      });
    
API.AI JSON
{
  "google": {
    "system_intent": {
      "intent": "actions.intent.TRANSACTION_DECISION",
      "data": {
        "@type": "type.googleapis.com/google.actions.v2.TransactionDecisionValueSpec",
        "proposedOrder": ...,
        "paymentOptions": {
          "actionProvidedOptions": {
            "paymentType": "PAYMENT_CARD",
            "displayName": "VISA-1234"
          }
        }
      }
    }
  }
}
Handle the user's decision

After the Assistant fulfills the intent, it sends your fulfillment a request with the actions_intent_TRANSACTION_DECISION intent with the user's answer to the transaction decision.

To properly handle this request, declare an API.AI intent that's triggered by the actions_intent_TRANSACTION_DECISION event. When triggered, handle it in your fulfillment in the following way.

API.AI Node.js
if (app.getTransactionDecision()
      && app.getTransactionDecision().userDecision ===
        app.Transactions.ConfirmationDecision.ACCEPTED) {
      let googleOrderId = app.getTransactionDecision().order.googleOrderId;
    

Confirm the order and send a receipt

When the actions.intent.TRANSACTION_DECISION intent returns with a userDecision of "ORDER_ACCEPTED", you must immediately perform whatever processing is required to "confirm" the order (likely including persisting it in your own database and charging the user). You must then send a Receipt in an OrderUpdate object in your next response. You can choose to either end the dialog or include a further prompt with the receipt. Once an order has been placed, Google will provide an order ID. Attempting to place that same order again will have no result.

When you provide this initial OrderUpdate, a "collapsed receipt card" will be displayed along with the rest of your response. This will mirror the receipt that the user will be able to find in their Order History.

During order confirmation, you must pass a confirmedActionOrderId. This can be the provided googleOrderId, or your own custom identifier. Note that it will be displayed to the user in their receipt.

Finally, as part of the OrderUpdate you should provide orderManagementActions. These manifest as URL buttons at the bottom of the order details that the user can find in their Assistant Order History. We require that you provide, at a minimum, a VIEW_DETAILS OrderManagementAction with each order. This should contain a deep-link to the representation of the order on your app or website. Note that OrderManagementActions can also be provided as part of the OrderUpdate you send via the Conversation Send API (see below).

API

API.AI Node.js
    app.tell(app.buildRichResponse().addOrderUpdate(
        app.buildOrderUpdate(googleOrderId, true)
          .setOrderState(app.Transactions.OrderState.CREATED, 'Order created')
          .setInfo(app.Transactions.OrderStateInfo.RECEIPT, {
            confirmedActionOrderId: ''
          }))
        .addSimpleResponse('Transaction completed! You\'re all set!'));
  

Send order updates

After the order has been confirmed, we require that you send order updates throughout the life of the order. For example, an order could go through the following steps, each of which requiring an update:

  1. CONFIRMED - order is confirmed by your app - i.e. it is active and being processed for fulfillment
  2. IN_TRANSIT - order has been shipped and is on its way for delivery
  3. FULFILLED - order has been delivered
  4. RETURNED - order has been returned by the user after delivery

Other possible order states are:

  • REJECTED - if your app was unable to process, charge, or otherwise "activate" the order
  • CREATED - order is accepted by the user and "created" from the perspective of your app but not yet confirmed, for example if manual processing is required
  • CANCELLED - order was cancelled by the user

You provide order updates by sending an HTTP POST request to the conversation send API. Important order updates will be surfaced as push notifications on the user's Google Assistant-enabled mobile devices.

Order update requests to the conversation send API are authorized by an access token that you can exchange for a service account key assocaited with your Actions Console project.

The order updates themselves refer to an order either by a Google-generated ID or an ID provided by you while the transaction is initially being created and confirmed.

The initial "order update" provided with a receipt immediately following the user's acceptance during the actions.intent.TRANSACTION_DECISION would contain the CREATED state. The order states in steps 2 through 5 would need to be provided after the conversation had already ended. For this, we provide an asynchronous HTTP API that you can POST order updates. This is referred to as the Conversation Send API.

Some order updates will result in a push notification being sent to the user's Assistant-enabled mobile devices. This is decided by Google based on the importance of the update.

Authorizing requests to the Conversation Send API

To POST an order update to the Conversation Send API, you should download a JSON service account key associated with your Actions Console project. Then, before calling the Conversation Send API, you can exchange this key for a bearer token that may be passed into the Authorization header of the HTTP request. You can perform this exchange using the Google API client libraries -- see the Node.js client library documentation for an example.

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

  1. Assuming you have an Actions Console project with the ID "example-project-1", go to this link: https://console.developers.google.com/apis/api/actions.googleapis.com/overview?project=example-project-1
  2. If you see an Enable button, click it. Otherwise, proceed to step 3
  3. Assuming the same Actions Console project ID, go to this link https://console.developers.google.com/apis/credentials?project=example-project-1
  4. Click Create credentials > Service Account Key
  5. Click the Select box under Service Account and click New Service Account
  6. Give the Service Account a name like "orderupdater" and the Role of Project Owner
  7. Select the JSON key type
  8. Click Create
  9. A JSON service account key will be downloaded to the local machine. You will need to read from this key in the application that sends asynchronous order updates

Once you've exchanged your service account key for an OAuth bearer token, you can use this to make authorized requests to the Conversation Send API. The URL of the Conversation Send API is:

POST https://actions.googleapis.com/v2/conversations:send

The following headers should be provided:

  1. "Authorization: Bearer $token" where $token is the OAuth bearer token you exchanged your service account key for
  2. "Content-Type: application/json"

The POST request should take a JSON body of the following format:

{
  "customPushMessage": {
    "orderUpdate": OrderUpdate
  }
}

The OrderUpdate follows the format documented here. The top-level required fields are:

  • Either googleOrderId or actionOrderId - you may use the latter if you previously included a receipt containing a confirmedActionOrderId
  • orderState - the actual order state
  • updateTime - the exact time that the state changed (Total seconds since 1970/01/01)
  • orderManagementActions - these will be reset with each order update
  • userNotification