Building Actions with Transactions

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, you have two options.

  • 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 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 with a supported payment gateway, 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.
  • While Sandbox mode is active, you must not charge the user or actually fulfill the order.
  • If you are using a Google-provided payment method, you must pass in your payment processor's sandbox tokenization parameters when Sandbox is on. e.g. a test public api key, instead of your live public api key

Review transactions intents

Depending on your transaction, you may need to request the following intents to be fulfilled throughout your conversation:

  • actions.intent.TRANSACTION_DECISION - Confirms the transaction with the user. You will pass payment method configuration details to Google along with this intent.
    • This intent silently triggers actions.intent.TRANSACTION_REQUIREMENTS_CHECK to ensure the user can participate in transactions.
  • actions.intent.TRANSACTION_REQUIREMENTS_CHECK - Ensures that a user can actually participate in transactions. This intent will always be triggered as part of actions.intent.TRANSACTION_DECISION, but can optionally be triggered earlier in the conversation for ensuring that a user doesn't go through creating an order and later finding out they can't transact. This intent is silent, meaning that it doesn't have a conversation with the user when triggered.
  • actions.intent.DELIVERY_ADDRESS (optional) - Obtains the user's delivery address. This intent can be useful for presenting nearby locations at the beginning of a conversation or obtaining an address towards the end of the transaction process.
  • actions.intent.SIGN_IN (optional) - 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 intent can be used to sign up users 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.

Transaction flow

When your Actions project handles transactions, it will use the following flow:

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 with a payment method.

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.

Building an app with transactions

For an example of a complete transactional flow, check out our transactions sample.

Set up an Actions project for transactions

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

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

  1. Create a new project or import an existing project.
  2. Navigate to Deploy > Directory information.
  3. Under step 4 for Privacy and Consent, check the box that says "Do your Actions perform Transactions?".

Manually check for transaction requirements (optional)

User experience

As soon as the user has indicated they wish to transact, we recommend triggering the actions.intent.TRANSACTION_REQUIREMENTS_CHECK intent to quickly ensure they will be able to perform a transaction. For example, when invoked your Action 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.

  • If 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.
    • If any errors resulting in the failure state can be fixed by the user, they will be prompted to resolve those issues on their device. If the conversation is taking place on an voice-only surface, a handoff will be initiated to the user's phone.

Fulfillment

If you want to manually 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 using the client library:

Node.js

conv.ask(new TransactionRequirements({
  orderOptions: {
    requestDeliveryAddress: false,
  },
  paymentOptions: {
    googleProvidedOptions: {
      prepaidCardDisallowed: false,
      supportedCardNetworks: ['VISA', 'AMEX'],
      // These will be provided by payment processor,
      // like Stripe, Braintree, or Vantiv.
      tokenizationParameters: {},
    },
  },
}));

Dialogflow JSON

"data": {
  "google": {
    "expectUserResponse": true,
    "isSsml": false,
    "noInputPrompts": [],
    "systemIntent": {
      "data": {
        "@type": "type.googleapis.com/google.actions.v2.TransactionRequirementsCheckSpec",
        "paymentOptions": {
          "googleProvidedOptions": {
            "prepaidCardDisallowed": false,
            "supportedCardNetworks": [
              "VISA",
              "AMEX"
            ],
            "tokenizationParameters": {
              "parameters": {},
              "tokenizationType": "PAYMENT_GATEWAY"
            }
          }
        }
      },
      "intent": "actions.intent.TRANSACTION_REQUIREMENTS_CHECK"
    }
  }
}
Checking requirements with your own payment method

You can check to see if a user satisfies transactions requirements when using your own payment method using the client library:

Node.js

conv.ask(new TransactionRequirements({
  orderOptions: {
    requestDeliveryAddress: false,
  },
  paymentOptions: {
    actionProvidedOptions: {
      displayName: 'VISA-1234',
      paymentType: 'PAYMENT_CARD',
    },
  },
}));

Dialogflow JSON

"data": {
  "google": {
    "expectUserResponse": true,
    "isSsml": false,
    "noInputPrompts": [],
    "systemIntent": {
      "data": {
        "@type": "type.googleapis.com/google.actions.v2.TransactionRequirementsCheckSpec",
        "paymentOptions": {
          "actionProvidedOptions": {
            "displayName": "VISA-1234",
            "paymentType": "PAYMENT_CARD"
          }
        }
      },
      "intent": "actions.intent.TRANSACTION_REQUIREMENTS_CHECK"
    }
  }
}
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 a Dialogflow intent that's triggered by the actions_intent_TRANSACTION_REQUIREMENTS_CHECK event. When triggered, handle it in your fulfillment using the client library:

Node.js

const arg = conv.arguments.get('TRANSACTION_REQUIREMENTS_CHECK_RESULT');
  if (arg && arg.resultType ==='OK') {
    // Normally take the user through cart building flow
    conv.ask(`Looks like you're good to go! ` +
      `Try saying "Get Delivery Address".`);
  } else {
    conv.close('Transaction failed.');
  }

Obtain a delivery address (optional)

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"

User experience

On surfaces with a screen, the user will choose which address they want to use for the transaction. If they haven't previously given an address, they'll be able to enter a new address.

On voice-only surfaces, the Assistant will ask the user for permission to share their default address for the transaction. If they haven't previously given an address, the conversation will be handed off to a phone for entry.

Requesting the delivery address

Node.js

conv.ask(new DeliveryAddress({
  addressOptions: {
    reason: 'To know where to send the order',
  },
}));

Dialogflow JSON

"data": {
  "google": {
    "expectUserResponse": true,
    "isSsml": false,
    "noInputPrompts": [],
    "systemIntent": {
      "data": {
        "@type": "type.googleapis.com/google.actions.v2.DeliveryAddressValueSpec",
        "addressOptions": {
          "reason": "To know where to send the order"
        }
      },
      "intent": "actions.intent.DELIVERY_ADDRESS"
    }
  }
}

Receiving the delivery address

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

To properly handle this request, declare a Dialogflow intent that's triggered by the actions_intent_DELIVERY_ADDRESSevent. When triggered, handle it in your fulfillment using the client library:

Node.js

const arg = conv.arguments.get('DELIVERY_ADDRESS_VALUE');
  if (arg.userDecision ==='ACCEPTED') {
    console.log('DELIVERY ADDRESS: ' +
      arg.location.postalAddress.addressLines[0]);
    conv.ask('Great, got your address! Now say "confirm transaction".');
  } else {
    conv.close('Transaction failed.');
  }

Build an order

User experience

Once you have the user information you need, you'll 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.

You could build a cart assembly experience that enables the user to re-order their most recent purchase via a simple yes or no question. You could also present the user a carousel or list card of the top "featured" or "recommended" items. We recommend using rich responses to present the user's options visually, but also design the conversation such that the user can build their cart using only their voice.

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

Fulfillment

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

Node.js

const order = {
  id: UNIQUE_ORDER_ID,
  cart: {
    merchant: {
      id: 'book_store_1',
      name: 'Book Store',
    },
    lineItems: [
      {
        name: 'My Memoirs',
        id: 'memoirs_1',
        price: {
          amount: {
            currencyCode: 'USD',
            nanos: 990000000,
            units: 3,
          },
          type: 'ACTUAL',
        },
        quantity: 1,
        subLines: [
          {
            note: 'Note from the author',
          },
        ],
        type: 'REGULAR',
      },
      {
        name: 'Memoirs of a person',
        id: 'memoirs_2',
        price: {
          amount: {
            currencyCode: 'USD',
            nanos: 990000000,
            units: 5,
          },
          type: 'ACTUAL',
        },
        quantity: 1,
        subLines: [
          {
            note: 'Special introduction by author',
          },
        ],
        type: 'REGULAR',
      },
      {
        name: 'Their memoirs',
        id: 'memoirs_3',
        price: {
          amount: {
            currencyCode: 'USD',
            nanos: 750000000,
            units: 15,
          },
          type: 'ACTUAL',
        },
        quantity: 1,
        subLines: [
          {
            lineItem: {
              name: 'Special memoir epilogue',
              id: 'memoirs_epilogue',
              price: {
                amount: {
                  currencyCode: 'USD',
                  nanos: 990000000,
                  units: 3,
                },
                type: 'ACTUAL',
              },
              quantity: 1,
              type: 'REGULAR',
            },
          },
        ],
        type: 'REGULAR',
      },
      {
        name: 'Our memoirs',
        id: 'memoirs_4',
        price: {
          amount: {
            currencyCode: 'USD',
            nanos: 490000000,
            units: 6,
          },
          type: 'ACTUAL',
        },
        quantity: 1,
        subLines: [
          {
            note: 'Special introduction by author',
          },
        ],
        type: 'REGULAR',
      },
    ],
    notes: 'The Memoir collection',
    otherItems: [
      {
        name: 'Subtotal',
        id: 'subtotal',
        price: {
          amount: {
            currencyCode: 'USD',
            nanos: 220000000,
            units: 32,
          },
          type: 'ESTIMATE',
        },
        type: 'SUBTOTAL',
      },
      {
        name: 'Tax',
        id: 'tax',
        price: {
          amount: {
            currencyCode: 'USD',
            nanos: 780000000,
            units: 2,
          },
          type: 'ESTIMATE',
        },
        type: 'TAX',
      },
    ],
  },
  otherItems: [],
  totalPrice: {
    amount: {
      currencyCode: 'USD',
      nanos: 0,
      units: 35,
    },
    type: 'ESTIMATE',
  },
};

Dialogflow JSON

"proposedOrder": {
  "cart": {
    "lineItems": [
      {
        "id": "My Memoirs",
        "name": "memoirs_1",
        "price": {
          "amount": {
            "currencyCode": "USD",
            "nanos": 990000000,
            "units": 3
          },
          "type": "ACTUAL"
        },
        "quantity": 1,
        "subLines": [
          {
            "note": "Note from the author"
          }
        ],
        "type": "REGULAR"
      },
      {
        "id": "Memoirs of a person",
        "name": "memoirs_2",
        "price": {
          "amount": {
            "currencyCode": "USD",
            "nanos": 990000000,
            "units": 5
          },
          "type": "ACTUAL"
        },
        "quantity": 1,
        "subLines": [
          {
            "note": "Special introduction by author"
          }
        ],
        "type": "REGULAR"
      },
      {
        "id": "Their memoirs",
        "name": "memoirs_3",
        "price": {
          "amount": {
            "currencyCode": "USD",
            "nanos": 750000000,
            "units": 15
          },
          "type": "ACTUAL"
        },
        "quantity": 1,
        "type": "REGULAR"
      },
      {
        "id": "Our memoirs",
        "name": "memoirs_4",
        "price": {
          "amount": {
            "currencyCode": "USD",
            "nanos": 490000000,
            "units": 6
          },
          "type": "ACTUAL"
        },
        "quantity": 1,
        "type": "REGULAR"
      }
    ],
    "merchant": {
      "id": "book_store_1",
      "name": "Book Store"
    },
    "notes": "The Memoir collection",
    "otherItems": []
  },
  "id": "<UNIQUE_ORDER_ID>",
  "otherItems": [
    {
      "id": "Subtotal",
      "name": "subtotal",
      "price": {
        "amount": {
          "currencyCode": "USD",
          "nanos": 220000000,
          "units": 32
        },
        "type": "ESTIMATE"
      },
      "type": "SUBTOTAL"
    },
    {
      "id": "Tax",
      "name": "tax",
      "price": {
        "amount": {
          "currencyCode": "USD",
          "nanos": 780000000,
          "units": 2
        },
        "type": "ESTIMATE"
      },
      "type": "TAX"
    }
  ],
  "totalPrice": {
    "amount": {
      "currencyCode": "USD",
      "nanos": 0,
      "units": 35
    },
    "type": "ESTIMATE"
  }
}

Perform account linking (optional)

When using your own payment method to charge the user, we recommend linking 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 use this intent 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.

Fulfillment

Requesting signin

Node.js

conv.ask(new SignIn())

Dialogflow JSON

"data": {
  "google": {
    "expectUserResponse": true,
    "isSsml": false,
    "noInputPrompts": [],
    "systemIntent": {
      "data": {},
      "intent": "actions.intent.SIGN_IN"
    }
  }
}
Receiving the sign in result

Node.js

app.intent('ask_for_sign_in_confirmation', (conv, params, signin) => {
  if (signin.status !== 'OK') {
    return conv.ask('You need to sign in before using the app.');
  }
  // const access = conv.user.access.token;
  // possibly do something with access token
  return conv.ask('Great! Thanks for signing in.');
})

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 Dialogflow intents to be ready for order change requests such as this.

Fulfillment

You must provide a TransactionDecisionValueSpec when you request the actions.intent.TRANSACTION_DECISION intent. This contains the ProposedOrder as well as your OrderOptionsand 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

Node.js

conv.ask(new TransactionDecision({
  orderOptions: {
    requestDeliveryAddress: false,
  },
  paymentOptions: {
    googleProvidedOptions: {
      prepaidCardDisallowed: false,
      supportedCardNetworks: ['VISA', 'AMEX'],
      // These will be provided by payment processor,
      // like Stripe, Braintree, or Vantiv.
      tokenizationParameters: {
        "gateway": "stripe",
        "stripe:publishableKey": (conv.sandbox ? "pk_test_key" : "pk_live_key"),
        "stripe:version": "2017-04-06"
      },
    },
  },
  proposedOrder: order,
}));

Dialogflow JSON

"google": {
  "expectUserResponse": true,
  "isSsml": false,
  "noInputPrompts": [],
  "systemIntent": {
    "data": {
      "@type": "type.googleapis.com/google.actions.v2.TransactionDecisionValueSpec",
      "paymentOptions": {
        "googleProvidedOptions": {
          "prepaidCardDisallowed": false,
          "supportedCardNetworks": [
            "VISA",
            "MASTERCARD"
          ],
          "tokenizationParameters": {
            "parameters": {
              "gateway": "<GATEWAY>",
              "stripe:publishableKey": "<PUBLISHABLE_KEY>",
              "stripe:version": "<API_VERSION>"
            },
            "tokenizationType": "PAYMENT_GATEWAY"
          }
        }
      },
      "proposedOrder": ...
    },
    "intent": "actions.intent.TRANSACTION_DECISION"
  }
}
Provide your own payment method

Node.js

conv.ask(new TransactionDecision({
  orderOptions: {
    requestDeliveryAddress: true,
  },
  paymentOptions: {
    actionProvidedOptions: {
      paymentType: 'PAYMENT_CARD',
      displayName: 'VISA-1234',
    },
  },
  proposedOrder: order,
}));

Dialogflow JSON

"data": {
  "google": {
    "expectUserResponse": true,
    "isSsml": false,
    "noInputPrompts": [],
    "systemIntent": {
      "data": {
        "@type": "type.googleapis.com/google.actions.v2.TransactionDecisionValueSpec",
        "orderOptions": {
          "requestDeliveryAddress": true
        },
        "paymentOptions": {
          "actionProvidedOptions": {
            "displayName": "VISA-1234",
            "paymentType": "PAYMENT_CARD"
          }
        },
        "proposedOrder": ...
      },
      "intent": "actions.intent.TRANSACTION_DECISION"
    }
  }
}
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 a Dialogflow intent that's triggered by the actions_intent_TRANSACTION_DECISION event. When triggered, handle it in your fulfillment using the client library:

Node.js

const arg = conv.arguments.get('TRANSACTION_DECISION_VALUE');
if (arg && arg.userDecision ==='ORDER_ACCEPTED') {
  const googleOrderId = arg.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).

Fulfillment

Node.js

conv.ask(new OrderUpdate({
  actionOrderId: googleOrderId,
  orderState: {
    label: 'Order created',
    state: 'CREATED',
  },
  receipt: {
    confirmedActionOrderId: '',
  },
}));
conv.ask(`Transaction completed! You're all set! Would you like to do anything
else?'`);

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 associated 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. 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
  2. If you see an Enable button, click it. Otherwise, proceed to step 3
  3. Follow this link, swapping "example-project-1" for your project ID: 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

Node.js


// Import the 'googleapis' module for authorizing the request.
const google = require('googleapis');

// Import the 'request' module for sending an HTTP POST request.
const request = require('request');

// Import the OrderUpdate class from the Actions on Google client library.
const {OrderUpdate} = require('actions-on-google');

// Import the service account key used to authorize the request. Replace the
// string path with a path to your service account key.
const key = require('./path/to/key.json');

// Create a new JWT client for the Actions API using credentials from the
// service account key.
let jwtClient = new google.auth.JWT(
  key.client_email,
  null,
  key.private_key,
  ['https://www.googleapis.com/auth/actions.fulfillment.conversation'],
  null
);

// Authorize the client asynchronously, passing in a callback to run
// upon authorization.
jwtClient.authorize((err, tokens) => {
  if (err) {
    console.log(err);
    return;
  }

  // Get the current time in ISO 8601 format.
  const currentTime = new Date().toISOString();

  // Declare the ID of the order to update.
  const actionOrderId = '<UNIQUE_ORDER_ID>';

  // Declare the particular updated state of the order.
  const orderUpdate = new OrderUpdate({
    actionOrderId: actionOrderId,
    orderState: {
      label: 'Order has been delivered!',
      state: 'FULFILLED',
    },
    updateTime: currentTime,
  });

  // Set up the POST request header and body, including the authorized token
  // and order update.
  const bearer = 'Bearer ' + tokens.access_token;
  const options = {
    method: 'POST',
    url: 'https://actions.googleapis.com/v2/conversations:send',
    headers: {
      'Authorization': bearer,
    },
    body: {
      custom_push_message: {
        order_update: orderUpdate,
      },
      // The line below should be removed for non-sandbox transactions.
      is_in_sandbox: true,
    },
    json: true,
  };

  // Send the POST request to the Actions API.
  request.post(options, (err, httpResponse, body) => {
    if (err) {
      console.log(err);
      return;
    }
    console.log(body);
  });
});

Troubleshooting

If your transaction fulfillment is failing for whatever reason, the logs in the Actions Console are a good place to start figuring out what's going wrong. You can learn more about troubleshooting in the Actions Console here.