Build Physical Transactions with Google Pay

This guide will walk you through the process of developing an Actions project that incorporates transactions for physical goods and uses Google Pay for payment.

Transaction flow

When your Actions project handles physical transactions using Google Pay, it will use the following flow:

  1. Gather information (optional) - Depending on the nature of your transaction, you may want to gather the following information from the user at the start of the conversation.
    1. Validate transaction requirements - You can use the transactions requirements helper at the start of the conversation to make sure the user's payment information is properly configured and available before the user builds their cart.
    2. Request a delivery address - If your transaction requires a delivery address, you can request fulfillment of the delivery address helper intent to gather one from the user.
  2. Build the order - You walk the user through a "cart assembly" where they pick which items they want to purchase.
  3. Propose the order - Once the cart is complete, you propose the order to the user so they can confirm it's correct. If the order is confirmed, you'll receive a response with order details and a payment token.
  4. Finalize the order and send a receipt - With the order confirmed, update your inventory tracking or other fulfillment services and then send a receipt to the user.
  5. Send order updates - Over the course of the order fulfillment's lifespan, you'll give the user order updates by sending POST requests to the Actions API.

Review guidelines

Keep in mind that additional policies apply to Actions with transactions. It can take us up to six weeks to review Actions with transactions, so factor that time in when planning your release schedule. To ease the review process, make sure you comply with the policies and guidelines for transactions before submitting your Action for review.

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

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

Build your project

For extensive examples of transactional conversations, view our transactions samples in Node.js and Java.

Setup

When creating your Action, you must specify that you want to perform transactions in the Actions console:

  1. Create a new project or import an existing project.
  2. Navigate to Deploy > Directory information.
  3. Under Additional information > Transactions > check the box that says "Do your Actions use the Transactions API to perform transactions of physical goods?".

1. Gather information (optional)

1a. Validate transaction requirements (optional)

User experience

As soon as the user has indicated they wish to make a purchase, 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. This will ensure that they can proceed and give them an opportunity to fix any settings preventing them from continuing with the transaction.

Requesting the transactions requirements check intent will result in one of the following outcomes:

  • If the requirements are met, the intent will be sent back to your fulfillment 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 fulfillment 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 a voice-only surface, a handoff will be initiated to the user's phone.
Fulfillment

To ensure that a user meets transaction requirements, request fulfillment of the actions.intent.TRANSACTION_REQUIREMENTS_CHECK intent with a TransactionRequirementsCheckSpec object.

Check requirements

You can check to see if a user satisfies transactions requirements by using the client library:

Node.js
conv.ask(new TransactionRequirements({
  orderOptions: {
    requestDeliveryAddress: false,
  },
  paymentOptions: {
    googleProvidedOptions: {
      prepaidCardDisallowed: false,
      supportedCardNetworks: ['VISA', 'AMEX', 'DISCOVER', 'MASTERCARD'],
      tokenizationParameters: {
        tokenizationType: 'PAYMENT_GATEWAY',
        // These will be provided by payment processor,
        // like Stripe, Braintree, Vantiv, Ayden, etc.
        parameters: {
          'gateway': 'stripe',
          'stripe:publishableKey': (conv.sandbox ? 'pk_test_key' : 'pk_live_key'),
          'stripe:version': '2018-11-08'
        },
      },
    },
  },
}));
Java
Map<String, String> parameters = new HashMap<>();
PaymentMethodTokenizationParameters tokenizationParameters =
    new PaymentMethodTokenizationParameters()
        .setTokenizationType("PAYMENT_GATEWAY")
        .setParameters(parameters);
GoogleProvidedPaymentOptions googleProvidedPaymentOptions =
    new GoogleProvidedPaymentOptions()
        .setPrepaidCardDisallowed(false)
        .setSupportedCardNetworks(Arrays.asList("VISA", "AMEX"))
        .setTokenizationParameters(tokenizationParameters);
OrderOptions orderOptions = new OrderOptions().setRequestDeliveryAddress(false);
PaymentOptions paymentOptions =
    new PaymentOptions().setGoogleProvidedOptions(googleProvidedPaymentOptions);

return getResponseBuilder(request)
    .add("Placeholder for transaction requirements text")
    .add(
        new TransactionRequirements()
            .setOrderOptions(orderOptions)
            .setPaymentOptions(paymentOptions))
    .build();
Dialogflow JSON

Note that the JSON below describes a webhook response.

{
  "payload": {
    "google": {
      "expectUserResponse": true,
      "richResponse": {
        "items": [
          {
            "simpleResponse": {
              "textToSpeech": "PLACEHOLDER"
            }
          }
        ]
      },
      "userStorage": "{\"data\":{}}",
      "systemIntent": {
        "intent": "actions.intent.TRANSACTION_REQUIREMENTS_CHECK",
        "data": {
          "@type": "type.googleapis.com/google.actions.v2.TransactionRequirementsCheckSpec",
          "orderOptions": {
            "requestDeliveryAddress": false
          },
          "paymentOptions": {
            "googleProvidedOptions": {
              "prepaidCardDisallowed": false,
              "supportedCardNetworks": [
                "VISA",
                "AMEX",
                "DISCOVER",
                "MASTERCARD"
              ],
              "tokenizationParameters": {
                "tokenizationType": "PAYMENT_GATEWAY",
                "parameters": {
                  "gateway": "stripe",
                  "stripe:publishableKey": "pk_test_key",
                  "stripe:version": "2018-11-08"
                }
              }
            }
          }
        }
      }
    }
  },
  "outputContexts": [
    {
      "name": "/contexts/_actions_on_google",
      "lifespanCount": 99,
      "parameters": {
        "data": "{}"
      }
    }
  ]
}
Actions SDK JSON

Note that the JSON below describes a webhook response.

{
  "expectUserResponse": true,
  "expectedInputs": [
    {
      "inputPrompt": {
        "richInitialPrompt": {
          "items": [
            {
              "simpleResponse": {
                "textToSpeech": "PLACEHOLDER"
              }
            }
          ]
        }
      },
      "possibleIntents": [
        {
          "intent": "actions.intent.TRANSACTION_REQUIREMENTS_CHECK",
          "inputValueData": {
            "@type": "type.googleapis.com/google.actions.v2.TransactionRequirementsCheckSpec",
            "orderOptions": {
              "requestDeliveryAddress": false
            },
            "paymentOptions": {
              "googleProvidedOptions": {
                "prepaidCardDisallowed": false,
                "supportedCardNetworks": [
                  "VISA",
                  "AMEX",
                  "DISCOVER",
                  "MASTERCARD"
                ],
                "tokenizationParameters": {
                  "tokenizationType": "PAYMENT_GATEWAY",
                  "parameters": {
                    "gateway": "stripe",
                    "stripe:publishableKey": "pk_live_key",
                    "stripe:version": "2018-11-08"
                  }
                }
              }
            }
          }
        }
      ]
    }
  ],
  "conversationToken": "{\"data\":{}}",
  "userStorage": "{\"data\":{}}"
}
Receive 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.');
  }
Java
Argument transactionCheckResult = request.getArgument("TRANSACTION_REQUIREMENTS_CHECK_RESULT");
boolean result = false;
if (transactionCheckResult != null) {
  Map<String, Object> map = transactionCheckResult.getExtension();
  if (map != null) {
    String resultType = (String) map.get("resultType");
    result = resultType != null && resultType.equals("OK");
  }
}
ResponseBuilder responseBuilder = getResponseBuilder(request);
if (result) {
  responseBuilder.add("Looks like you're good to go! Try saying 'Get Delivery Address'");
} else {
  responseBuilder.add("Transaction failed");
}
return responseBuilder.build();
Dialogflow JSON

Note that the JSON below describes a webhook request.

{
  "responseId": "",
  "queryResult": {
    "queryText": "",
    "action": "",
    "parameters": {},
    "allRequiredParamsPresent": true,
    "fulfillmentText": "",
    "fulfillmentMessages": [],
    "outputContexts": [],
    "intent": {
      "name": "transaction_check_confirm_df",
      "displayName": "transaction_check_confirm_df"
    },
    "intentDetectionConfidence": 1,
    "diagnosticInfo": {},
    "languageCode": ""
  },
  "originalDetectIntentRequest": {
    "source": "google",
    "version": "2",
    "payload": {
      "isInSandbox": true,
      "surface": {
        "capabilities": [
          {
            "name": "actions.capability.SCREEN_OUTPUT"
          },
          {
            "name": "actions.capability.AUDIO_OUTPUT"
          },
          {
            "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
          },
          {
            "name": "actions.capability.WEB_BROWSER"
          }
        ]
      },
      "inputs": [
        {
          "rawInputs": [],
          "intent": "",
          "arguments": [
            {
              "extension": {
                "@type": "type.googleapis.com/google.actions.v2.TransactionRequirementsCheckResult",
                "resultType": "OK"
              },
              "name": "TRANSACTION_REQUIREMENTS_CHECK_RESULT"
            }
          ]
        }
      ],
      "user": {},
      "conversation": {},
      "availableSurfaces": [
        {
          "capabilities": [
            {
              "name": "actions.capability.SCREEN_OUTPUT"
            },
            {
              "name": "actions.capability.AUDIO_OUTPUT"
            },
            {
              "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
            },
            {
              "name": "actions.capability.WEB_BROWSER"
            }
          ]
        }
      ]
    }
  },
  "session": ""
}
Actions SDK JSON

Note that the JSON below describes a webhook request.

{
  "user": {},
  "device": {},
  "surface": {
    "capabilities": [
      {
        "name": "actions.capability.SCREEN_OUTPUT"
      },
      {
        "name": "actions.capability.AUDIO_OUTPUT"
      },
      {
        "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
      },
      {
        "name": "actions.capability.WEB_BROWSER"
      }
    ]
  },
  "conversation": {},
  "inputs": [
    {
      "rawInputs": [],
      "intent": "transaction_check_confirm_asdk",
      "arguments": [
        {
          "extension": {
            "@type": "type.googleapis.com/google.actions.v2.TransactionRequirementsCheckResult",
            "resultType": "OK"
          },
          "name": "TRANSACTION_REQUIREMENTS_CHECK_RESULT"
        }
      ]
    }
  ],
  "availableSurfaces": [
    {
      "capabilities": [
        {
          "name": "actions.capability.SCREEN_OUTPUT"
        },
        {
          "name": "actions.capability.AUDIO_OUTPUT"
        },
        {
          "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
        },
        {
          "name": "actions.capability.WEB_BROWSER"
        }
      ]
    }
  ]
}

1b. Request 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 to 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.

Request the address
Node.js
conv.ask(new DeliveryAddress({
  addressOptions: {
    reason: 'To know where to send the order',
  },
}));
Java
DeliveryAddressValueSpecAddressOptions addressOptions =
    new DeliveryAddressValueSpecAddressOptions().setReason("To know where to send the order");
return getResponseBuilder(request)
    .add("Placeholder for delivery address text")
    .add(new DeliveryAddress().setAddressOptions(addressOptions))
    .build();
JSON

Note that the JSON below describes a webhook response.

{
  "payload": {
    "google": {
      "expectUserResponse": true,
      "richResponse": {
        "items": [
          {
            "simpleResponse": {
              "textToSpeech": "PLACEHOLDER"
            }
          }
        ]
      },
      "userStorage": "{\"data\":{}}",
      "systemIntent": {
        "intent": "actions.intent.DELIVERY_ADDRESS",
        "data": {
          "@type": "type.googleapis.com/google.actions.v2.DeliveryAddressValueSpec",
          "addressOptions": {
            "reason": "To know where to send the order"
          }
        }
      }
    }
  },
  "outputContexts": [
    {
      "name": "/contexts/_actions_on_google",
      "lifespanCount": 99,
      "parameters": {
        "data": "{}"
      }
    }
  ]
}
Receive the 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 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 && 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.');
  }
Java
Argument deliveryAddressValue = request.getArgument("DELIVERY_ADDRESS_VALUE");
Location deliveryAddress = null;
if (deliveryAddressValue != null) {
  Map<String, Object> map = deliveryAddressValue.getExtension();
  if (map != null) {
    String userDecision = (String) map.get("userDecision");
    Location location = (Location) map.get("location");
    deliveryAddress = userDecision != null && userDecision.equals("ACCEPTED") ? location : null;
  }
}
ResponseBuilder responseBuilder = getResponseBuilder(request);
if (deliveryAddress != null) {
  responseBuilder.add("Great, got your address! Now say 'confirm transaction'.");
} else {
  responseBuilder.add("Transaction failed.").endConversation();
}
return responseBuilder.build();
JSON

Note that the JSON below describes a webhook request.

{
  "responseId": "",
  "queryResult": {
    "queryText": "",
    "action": "",
    "parameters": {},
    "allRequiredParamsPresent": true,
    "fulfillmentText": "",
    "fulfillmentMessages": [],
    "outputContexts": [],
    "intent": {
      "name": "delivery_address_confirm_df",
      "displayName": "delivery_address_confirm_df"
    },
    "intentDetectionConfidence": 1,
    "diagnosticInfo": {},
    "languageCode": ""
  },
  "originalDetectIntentRequest": {
    "source": "google",
    "version": "2",
    "payload": {
      "isInSandbox": true,
      "surface": {
        "capabilities": [
          {
            "name": "actions.capability.SCREEN_OUTPUT"
          },
          {
            "name": "actions.capability.AUDIO_OUTPUT"
          },
          {
            "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
          },
          {
            "name": "actions.capability.WEB_BROWSER"
          }
        ]
      },
      "inputs": [
        {
          "rawInputs": [],
          "intent": "",
          "arguments": [
            {
              "extension": {
                "userDecision": "ACCEPTED",
                "@type": "type.googleapis.com/google.actions.v2.DeliveryAddressValue",
                "location": {
                  "zipCode": "94043",
                  "postalAddress": {
                    "regionCode": "US",
                    "recipients": [
                      "John Smith"
                    ],
                    "postalCode": "94043",
                    "locality": "MOUNTAIN VIEW",
                    "addressLines": [
                      "1600 AMPHITHEATRE PKWY"
                    ],
                    "administrativeArea": "CA"
                  },
                  "phoneNumber": "+1 202-222-2222",
                  "city": "MOUNTAIN VIEW",
                  "coordinates": {
                    "latitude": 37.432524,
                    "longitude": -122.098545
                  }
                }
              },
              "name": "DELIVERY_ADDRESS_VALUE"
            }
          ]
        }
      ],
      "user": {},
      "conversation": {},
      "availableSurfaces": [
        {
          "capabilities": [
            {
              "name": "actions.capability.SCREEN_OUTPUT"
            },
            {
              "name": "actions.capability.AUDIO_OUTPUT"
            },
            {
              "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
            },
            {
              "name": "actions.capability.WEB_BROWSER"
            }
          ]
        }
      ]
    }
  },
  "session": ""
}

2. Build the 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 Action 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 object 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 Node.js client library and Java client library reference documentation.

Node.js
const order = {
    id: 'UNIQUE_ORDER_ID222',
    cart: {
      merchant: {
        id: 'book_store_id',
        name: 'A Book Store',
      },
      lineItems: [
        {
          name: 'My Memoirs',
          id: 'mymemoirs_id',
          price: {
            amount: {
              currencyCode: 'USD',
              nanos: 990000000,
              units: 8,
            },
            type: 'ACTUAL',
          },
          quantity: 1,
          subLines: [
            {
              note: 'By Bestselling Novelist',
            },
          ],
          type: 'REGULAR',
        },
        {
          name: 'Biography',
          id: 'biography_id',
          price: {
            amount: {
              currencyCode: 'USD',
              nanos: 990000000,
              units: 10,
            },
            type: 'ACTUAL',
          },
          quantity: 1,
          subLines: [
            {
              note: 'Signed copy',
            },
          ],
          type: 'REGULAR',
        },
      ],
      notes: 'Sale event',
      otherItems: [],
    },
    otherItems: [
      {
        name: 'Subtotal',
        id: 'subtotal',
        price: {
          amount: {
            currencyCode: 'USD',
            nanos: 980000000,
            units: 19,
          },
          type: 'ESTIMATE',
        },
        type: 'SUBTOTAL',
      },
      {
        name: 'Tax',
        id: 'tax',
        price: {
          amount: {
            currencyCode: 'USD',
            nanos: 780000000,
            units: 2,
          },
          type: 'ESTIMATE',
        },
        type: 'TAX',
      },
    ],
    totalPrice: {
      amount: {
        currencyCode: 'USD',
        nanos: 760000000,
        units: 22,
      },
      type: 'ESTIMATE',
    },
  };
Java
Argument transactionDecisionValue = request.getArgument("TRANSACTION_DECISION_VALUE");
Map<String, Object> extension = null;
if (transactionDecisionValue != null) {
  extension = transactionDecisionValue.getExtension();
}
String userDecision = null;
if (extension != null) {
  userDecision = (String) extension.get("userDecision");
}
if ((userDecision != null && userDecision.equals("ORDER_ACCEPTED"))) {
  String finalOrderId = ((Order) extension.get("order")).getFinalOrder().getId();
}
JSON

Note that the JSON below describes a webhook response.

{
  "payload": {
    "google": {
      "expectUserResponse": true,
      "richResponse": {
        "items": [
          {
            "simpleResponse": {
              "textToSpeech": "PLACEHOLDER"
            }
          }
        ]
      },
      "userStorage": "{\"data\":{}}",
      "systemIntent": {
        "intent": "actions.intent.TRANSACTION_DECISION",
        "data": {
          "@type": "type.googleapis.com/google.actions.v2.TransactionDecisionValueSpec",
          "orderOptions": {
            "requestDeliveryAddress": true
          },
          "paymentOptions": {
            "actionProvidedOptions": {
              "paymentType": "PAYMENT_CARD",
              "displayName": "VISA-1234"
            }
          },
          "proposedOrder": {
            "id": "UNIQUE_ORDER_ID222",
            "cart": {
              "merchant": {
                "id": "book_store_id",
                "name": "A Book Store"
              },
              "lineItems": [
                {
                  "name": "My Memoirs",
                  "id": "mymemoirs_id",
                  "price": {
                    "amount": {
                      "currencyCode": "USD",
                      "nanos": 990000000,
                      "units": 8
                    },
                    "type": "ACTUAL"
                  },
                  "quantity": 1,
                  "subLines": [
                    {
                      "note": "By Bestselling Novelist"
                    }
                  ],
                  "type": "REGULAR"
                },
                {
                  "name": "Biography",
                  "id": "biography_id",
                  "price": {
                    "amount": {
                      "currencyCode": "USD",
                      "nanos": 990000000,
                      "units": 10
                    },
                    "type": "ACTUAL"
                  },
                  "quantity": 1,
                  "subLines": [
                    {
                      "note": "Signed copy"
                    }
                  ],
                  "type": "REGULAR"
                }
              ],
              "notes": "Sale event",
              "otherItems": []
            },
            "otherItems": [
              {
                "name": "Subtotal",
                "id": "subtotal",
                "price": {
                  "amount": {
                    "currencyCode": "USD",
                    "nanos": 980000000,
                    "units": 19
                  },
                  "type": "ESTIMATE"
                },
                "type": "SUBTOTAL"
              },
              {
                "name": "Tax",
                "id": "tax",
                "price": {
                  "amount": {
                    "currencyCode": "USD",
                    "nanos": 780000000,
                    "units": 2
                  },
                  "type": "ESTIMATE"
                },
                "type": "TAX"
              }
            ],
            "totalPrice": {
              "amount": {
                "currencyCode": "USD",
                "nanos": 760000000,
                "units": 22
              },
              "type": "ESTIMATE"
            }
          }
        }
      }
    }
  },
  "outputContexts": [
    {
      "name": "/contexts/_actions_on_google",
      "lifespanCount": 99,
      "parameters": {
        "data": "{}"
      }
    }
  ]
}

3. Propose the order

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. The data presented in the "cart preview card" is rendered as-is according to your ProposedOrder object contained in the order object.

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.

Fulfillment

When you request the actions.intent.TRANSACTION_DECISION intent, you'll create a TransactionDecision that contains the ProposedOrder as well as your OrderOptionsand PaymentOptions.

Your PaymentOptions object will include tokenizationParameters that change depending on which Google Pay processor you plan on using (such as Stripe, Braintree, ACI, etc).

The following code shows an example for a payment using Stripe:

Node.js
conv.ask(new TransactionDecision({
  orderOptions: {
    requestDeliveryAddress: false,
  },
  paymentOptions: {
    googleProvidedOptions: {
      prepaidCardDisallowed: false,
      supportedCardNetworks: ['VISA', 'AMEX', 'DISCOVER', 'MASTERCARD'],
      tokenizationParameters: {
        tokenizationType: 'PAYMENT_GATEWAY',
        // These will be provided by payment processor,
        // like Stripe, Braintree, Vantiv, Ayden, etc.
        parameters: {
          'gateway': 'stripe',
          'stripe:publishableKey': (conv.sandbox ? 'pk_test_key' : 'pk_live_key'),
          'stripe:version': '2018-11-08'
        },
      },
    },
  },
  proposedOrder: order,
}));
Java
OrderOptions orderOptions;
PaymentOptions paymentOptions;

// Setup Google provided payment options
Map<String, String> parameters = new HashMap<>();
parameters.put("gateway", "stripe");
parameters.put("stripe:publishableKey", request.isInSandbox() ? "pk_test_key" : "pk_live_key");
parameters.put("stripe:version", "2017-04-06");
PaymentMethodTokenizationParameters tokenizationParameters =
    new PaymentMethodTokenizationParameters()
        .setTokenizationType("PAYMENT_GATEWAY")
        .setParameters(parameters);
orderOptions = new OrderOptions().setRequestDeliveryAddress(false);
GoogleProvidedPaymentOptions googleProvidedPaymentOptions =
    new GoogleProvidedPaymentOptions()
        .setPrepaidCardDisallowed(false)
        .setSupportedCardNetworks(Arrays.asList("VISA", "AMEX"))
        .setTokenizationParameters(tokenizationParameters);
paymentOptions = new PaymentOptions().setGoogleProvidedOptions(googleProvidedPaymentOptions);

return getResponseBuilder(request)
    .add("Placeholder for transaction decision text")
    .add(
        new TransactionDecision()
            .setOrderOptions(orderOptions)
            .setPaymentOptions(paymentOptions)
            .setProposedOrder(proposedOrder))
    .build();
Dialogflow JSON

Note that the JSON below describes a webhook response.

{
  "payload": {
    "google": {
      "expectUserResponse": true,
      "richResponse": {
        "items": [
          {
            "simpleResponse": {
              "textToSpeech": "PLACEHOLDER"
            }
          }
        ]
      },
      "userStorage": "{\"data\":{}}",
      "systemIntent": {
        "intent": "actions.intent.TRANSACTION_DECISION",
        "data": {
          "@type": "type.googleapis.com/google.actions.v2.TransactionDecisionValueSpec",
          "orderOptions": {
            "requestDeliveryAddress": false
          },
          "paymentOptions": {
            "googleProvidedOptions": {
              "prepaidCardDisallowed": false,
              "supportedCardNetworks": [
                "VISA",
                "AMEX",
                "DISCOVER",
                "MASTERCARD"
              ],
              "tokenizationParameters": {
                "tokenizationType": "PAYMENT_GATEWAY",
                "parameters": {
                  "gateway": "stripe",
                  "stripe:publishableKey": "pk_test_key",
                  "stripe:version": "2018-11-08"
                }
              }
            }
          },
          "proposedOrder": {
            "id": "UNIQUE_ORDER_ID222",
            "cart": {
              "merchant": {
                "id": "book_store_id",
                "name": "A Book Store"
              },
              "lineItems": [
                {
                  "name": "My Memoirs",
                  "id": "mymemoirs_id",
                  "price": {
                    "amount": {
                      "currencyCode": "USD",
                      "nanos": 990000000,
                      "units": 8
                    },
                    "type": "ACTUAL"
                  },
                  "quantity": 1,
                  "subLines": [
                    {
                      "note": "By Bestselling Novelist"
                    }
                  ],
                  "type": "REGULAR"
                },
                {
                  "name": "Biography",
                  "id": "biography_id",
                  "price": {
                    "amount": {
                      "currencyCode": "USD",
                      "nanos": 990000000,
                      "units": 10
                    },
                    "type": "ACTUAL"
                  },
                  "quantity": 1,
                  "subLines": [
                    {
                      "note": "Signed copy"
                    }
                  ],
                  "type": "REGULAR"
                }
              ],
              "notes": "Sale event",
              "otherItems": []
            },
            "otherItems": [
              {
                "name": "Subtotal",
                "id": "subtotal",
                "price": {
                  "amount": {
                    "currencyCode": "USD",
                    "nanos": 980000000,
                    "units": 19
                  },
                  "type": "ESTIMATE"
                },
                "type": "SUBTOTAL"
              },
              {
                "name": "Tax",
                "id": "tax",
                "price": {
                  "amount": {
                    "currencyCode": "USD",
                    "nanos": 780000000,
                    "units": 2
                  },
                  "type": "ESTIMATE"
                },
                "type": "TAX"
              }
            ],
            "totalPrice": {
              "amount": {
                "currencyCode": "USD",
                "nanos": 760000000,
                "units": 22
              },
              "type": "ESTIMATE"
            }
          }
        }
      }
    }
  },
  "outputContexts": [
    {
      "name": "/contexts/_actions_on_google",
      "lifespanCount": 99,
      "parameters": {
        "data": "{}"
      }
    }
  ]
}
Actions SDK JSON

Note that the JSON below describes a webhook response.

{
  "expectUserResponse": true,
  "expectedInputs": [
    {
      "inputPrompt": {
        "richInitialPrompt": {
          "items": [
            {
              "simpleResponse": {
                "textToSpeech": "PLACEHOLDER"
              }
            }
          ]
        }
      },
      "possibleIntents": [
        {
          "intent": "actions.intent.TRANSACTION_DECISION",
          "inputValueData": {
            "@type": "type.googleapis.com/google.actions.v2.TransactionDecisionValueSpec",
            "orderOptions": {
              "requestDeliveryAddress": false
            },
            "paymentOptions": {
              "googleProvidedOptions": {
                "prepaidCardDisallowed": false,
                "supportedCardNetworks": [
                  "VISA",
                  "AMEX",
                  "DISCOVER",
                  "MASTERCARD"
                ],
                "tokenizationParameters": {
                  "tokenizationType": "PAYMENT_GATEWAY",
                  "parameters": {
                    "gateway": "stripe",
                    "stripe:publishableKey": "pk_live_key",
                    "stripe:version": "2017-04-06"
                  }
                }
              }
            },
            "proposedOrder": {
              "id": "UNIQUE_ORDER_ID222",
              "cart": {
                "merchant": {
                  "id": "book_store_id",
                  "name": "A Book Store"
                },
                "lineItems": [
                  {
                    "name": "My Memoirs",
                    "id": "mymemoirs_id",
                    "price": {
                      "amount": {
                        "currencyCode": "USD",
                        "nanos": 990000000,
                        "units": 8
                      },
                      "type": "ACTUAL"
                    },
                    "quantity": 1,
                    "subLines": [
                      {
                        "note": "By Bestselling Novelist"
                      }
                    ],
                    "type": "REGULAR"
                  },
                  {
                    "name": "Biography",
                    "id": "biography_id",
                    "price": {
                      "amount": {
                        "currencyCode": "USD",
                        "nanos": 990000000,
                        "units": 10
                      },
                      "type": "ACTUAL"
                    },
                    "quantity": 1,
                    "subLines": [
                      {
                        "note": "Signed copy"
                      }
                    ],
                    "type": "REGULAR"
                  }
                ],
                "notes": "Sale event",
                "otherItems": []
              },
              "otherItems": [
                {
                  "name": "Subtotal",
                  "id": "subtotal",
                  "price": {
                    "amount": {
                      "currencyCode": "USD",
                      "nanos": 980000000,
                      "units": 19
                    },
                    "type": "ESTIMATE"
                  },
                  "type": "SUBTOTAL"
                },
                {
                  "name": "Tax",
                  "id": "tax",
                  "price": {
                    "amount": {
                      "currencyCode": "USD",
                      "nanos": 780000000,
                      "units": 2
                    },
                    "type": "ESTIMATE"
                  },
                  "type": "TAX"
                }
              ],
              "totalPrice": {
                "amount": {
                  "currencyCode": "USD",
                  "nanos": 760000000,
                  "units": 22
                },
                "type": "ESTIMATE"
              }
            }
          }
        }
      ]
    }
  ],
  "conversationToken": "{\"data\":{}}",
  "userStorage": "{\"data\":{}}"
}

The contents of the parameters object will be different for each payment gateway. The following table shows the parameters used by each gateway:

EXAMPLE
"parameters": {
  "gateway": "example",
  "gatewayMerchantId": "exampleGatewayMerchantId"
}
ACI
"parameters": {
  "gateway": "aciworldwide",
  "gatewayMerchantId": "YOUR_ENTITY_ID"
}
ADYEN
"parameters": {
  "gateway": "adyen",
  "gatewayMerchantId": "YOUR_MERCHANT_ACCOUNT_NAME"
}
ALFA-BANK
"parameters": {
  "gateway": "alfabank",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
BLUE_MEDIA
"parameters": {
  "gateway": "bluemedia",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
BLUESNAP
"parameters": {
  "gateway": "bluesnap",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
BRAINTREE
"parameters": {
  "gateway": "braintree",
  "braintree:apiVersion": "v1",
  "braintree:sdkVersion": braintree.client.VERSION,
  "braintree:merchantId": "YOUR_BRAINTREE_MERCHANT_ID",
  "braintree:clientKey": "YOUR_BRAINTREE_TOKENIZATION_KEY"
}
CHASE_PAYMENTECH
"parameters": {
  "gateway": "chase",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ACCOUNT_NUMBER"
}
CHECKOUT
"parameters": {
  "gateway": "checkoutltd",
  "gatewayMerchantId": "YOUR_PUBLIC_KEY"
}
CLOUDPAYMENTS
"parameters": {
  "gateway": "cloudpayments",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
CYBERSOURCE
"parameters": {
  "gateway": "cybersource",
  "gatewayMerchantId": "YOUR_MERCHANT_ID"
}
DATATRANS
"parameters": {
  "gateway": "datatrans",
  "gatewayMerchantId": "YOUR_MERCHANT_ID"
}
EBANX
"parameters": {
  "gateway": "ebanx",
  "gatewayMerchantId": "YOUR_PUBLIC_INTEGRATION_KEY"
}
FIRST_DATA
"parameters": {
  "gateway": "firstdata",
  "gatewayMerchantId": "YOUR_MERCHANT_ID"
}
GLOBAL_PAYMENTS
"parameters": {
  "gateway": "globalpayments",
  "gatewayMerchantId": "YOUR_MERCHANT_ID"
}
GOPAY
"parameters": {
  "gateway": "gopay",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
HITRUST
"parameters": {
  "gateway": "hitrustpay",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
IMSOLUTIONS
"parameters": {
  "gateway": "imsolutions",
  "gatewayMerchantId": "YOUR_MERCHANT_ID"
}
LYRA
"parameters": {
  "gateway": "lyra",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
MPGS
"parameters": {
  "gateway": "mpgs",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
MONEY_MAIL_RU
"parameters": {
  "gateway": "moneymailru",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
NEWEBPAY
"parameters": {
  "gateway": "newebpay",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
NEXI
"parameters": {
  "gateway": "nexi",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
NMI
"parameters": {
  "gateway": "creditcall",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
PAYSAFE
"parameters": {
  "gateway": "paysafe",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
PAYTURE
"parameters": {
  "gateway": "payture",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
PAYU
"parameters": {
  "gateway": "payu",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
PRZELEWY24
"parameters": {
  "gateway": "przelewy24",
  "gatewayMerchantId": "YOUR_MERCHANT_ID"
}
RBKMONEY
"parameters": {
  "gateway": "rbkmoney",
  "gatewayMerchantId": "YOUR_MERCHANT_ID"
}
SBERBANK
"parameters": {
  "gateway": "sberbank",
  "gatewayMerchantId": "YOUR_ORGANIZATION_NAME"
}
SQUARE
"parameters": {
  "gateway": "square",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
STRIPE
"parameters": {
  "gateway": "stripe",
  "stripe:version": "2018-10-31",
  "stripe:publishableKey": "YOUR_PUBLIC_STRIPE_KEY"
}
TAPPAY
"parameters": {
  "gateway": "tappay",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
TINKOFF
"parameters": {
  "gateway": "tinkoff",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
UNITELLER
"parameters": {
  "gateway": "uniteller",
  "gatewayMerchantId": "YOUR_GATEWAY_MERCHANT_ID"
}
VANTIV
"parameters": {
  "gateway": "vantiv",
  "vantiv:merchantPayPageId": "YOUR_PAY_PAGE_ID",
  "vantiv:merchantOrderId": "YOUR_ORDER_ID",
  "vantiv:merchantTransactionId": "YOUR_TRANSACTION_ID",
  "vantiv:merchantReportGroup": "*web"
}
WORLDPAY
"parameters": {
  "gateway": "worldpay",
  "gatewayMerchantId": "YOUR_WORLDPAY_MERCHANT_ID"
}
YANDEX
"parameters": {
  "gateway": "yandexcheckout",
  "gatewayMerchantId": "YOUR_SHOP_ID"
}
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. You will receive an Argument containing a TransactionDecisionValue object, which will contain: checkResult, userDecision, order, and deliveryAddress.

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;
}
Java
Argument transactionDecisionValue = request.getArgument("TRANSACTION_DECISION_VALUE");
Map<String, Object> extension = null;
if (transactionDecisionValue != null) {
  extension = transactionDecisionValue.getExtension();
}
String userDecision = null;
if (extension != null) {
  userDecision = (String) extension.get("userDecision");
}
if ((userDecision != null && userDecision.equals("ORDER_ACCEPTED"))) {
  String finalOrderId = ((Order) extension.get("order")).getFinalOrder().getId();
}
Dialogflow JSON

Note that the JSON below describes a webhook request.

{
  "responseId": "",
  "queryResult": {
    "queryText": "",
    "action": "",
    "parameters": {},
    "allRequiredParamsPresent": true,
    "fulfillmentText": "",
    "fulfillmentMessages": [],
    "outputContexts": [],
    "intent": {
      "name": "get_transaction_decision_df",
      "displayName": "get_transaction_decision_df"
    },
    "intentDetectionConfidence": 1,
    "diagnosticInfo": {},
    "languageCode": ""
  },
  "originalDetectIntentRequest": {
    "source": "google",
    "version": "2",
    "payload": {
      "isInSandbox": true,
      "surface": {
        "capabilities": [
          {
            "name": "actions.capability.SCREEN_OUTPUT"
          },
          {
            "name": "actions.capability.AUDIO_OUTPUT"
          },
          {
            "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
          },
          {
            "name": "actions.capability.WEB_BROWSER"
          }
        ]
      },
      "inputs": [
        {
          "rawInputs": [],
          "intent": "",
          "arguments": [
            {
              "extension": {
                "userDecision": "ORDER_ACCEPTED",
                "@type": "type.googleapis.com/google.actions.v2.TransactionDecisionValue",
                "checkResult": {
                  "resultType": "OK"
                },
                "order": {
                  "finalOrder": {
                    "extension": {
                      "@type": "type.googleapis.com/google.actions.v2.orders.GenericExtension",
                      "locations": [
                        {
                          "location": {
                            "postalAddress": {
                              "regionCode": "US",
                              "recipients": [
                                "John Smith"
                              ],
                              "postalCode": "94043",
                              "locality": "MOUNTAIN VIEW",
                              "addressLines": [
                                "1600 AMPHITHEATRE PKWY"
                              ],
                              "administrativeArea": "CA"
                            }
                          },
                          "type": "DELIVERY"
                        }
                      ]
                    },
                    "totalPrice": {
                      "amount": {
                        "nanos": 760000000,
                        "units": "22",
                        "currencyCode": "USD"
                      },
                      "type": "ESTIMATE"
                    },
                    "id": "UNIQUE_ORDER_ID222",
                    "cart": {
                      "lineItems": [
                        {
                          "quantity": 1,
                          "subLines": [
                            {
                              "note": "Note from the author"
                            }
                          ],
                          "price": {
                            "amount": {
                              "nanos": 990000000,
                              "units": "8",
                              "currencyCode": "USD"
                            },
                            "type": "ACTUAL"
                          },
                          "name": "My Memoirs",
                          "id": "mymemoirs_title",
                          "type": "REGULAR"
                        },
                        {
                          "quantity": 1,
                          "subLines": [
                            {
                              "note": "Special introduction by author"
                            }
                          ],
                          "price": {
                            "amount": {
                              "nanos": 990000000,
                              "units": "10",
                              "currencyCode": "USD"
                            },
                            "type": "ACTUAL"
                          },
                          "name": "Memoirs of a person",
                          "id": "memoirs_2",
                          "type": "REGULAR"
                        }
                      ],
                      "notes": "The Memoir collection",
                      "merchant": {
                        "name": "A Book Store",
                        "id": "book_store_1"
                      },
                      "otherItems": [
                        {
                          "price": {
                            "amount": {
                              "nanos": 980000000,
                              "units": "19",
                              "currencyCode": "USD"
                            },
                            "type": "ESTIMATE"
                          },
                          "name": "Subtotal",
                          "id": "subtotal",
                          "type": "SUBTOTAL"
                        },
                        {
                          "price": {
                            "amount": {
                              "nanos": 780000000,
                              "units": "2",
                              "currencyCode": "USD"
                            },
                            "type": "ESTIMATE"
                          },
                          "name": "Tax",
                          "id": "tax",
                          "type": "TAX"
                        }
                      ]
                    }
                  },
                  "orderDate": "2018-12-19T20:43:39.073Z",
                  "paymentInfo": {
                    "displayName": "VISA-1234",
                    "paymentType": "PAYMENT_CARD"
                  },
                  "googleOrderId": "02622302589300277076"
                }
              },
              "name": "TRANSACTION_DECISION_VALUE"
            }
          ]
        }
      ],
      "user": {},
      "conversation": {},
      "availableSurfaces": [
        {
          "capabilities": [
            {
              "name": "actions.capability.SCREEN_OUTPUT"
            },
            {
              "name": "actions.capability.AUDIO_OUTPUT"
            },
            {
              "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
            },
            {
              "name": "actions.capability.WEB_BROWSER"
            }
          ]
        }
      ]
    }
  },
  "session": ""
}
Actions SDK JSON

Note that the JSON below describes a webhook request.

{
  "user": {},
  "device": {},
  "surface": {
    "capabilities": [
      {
        "name": "actions.capability.SCREEN_OUTPUT"
      },
      {
        "name": "actions.capability.AUDIO_OUTPUT"
      },
      {
        "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
      },
      {
        "name": "actions.capability.WEB_BROWSER"
      }
    ]
  },
  "conversation": {},
  "inputs": [
    {
      "rawInputs": [],
      "intent": "get_transaction_decision_asdk",
      "arguments": [
        {
          "extension": {
            "userDecision": "ORDER_ACCEPTED",
            "@type": "type.googleapis.com/google.actions.v2.TransactionDecisionValue",
            "checkResult": {
              "resultType": "OK"
            },
            "order": {
              "finalOrder": {
                "extension": {
                  "@type": "type.googleapis.com/google.actions.v2.orders.GenericExtension",
                  "locations": [
                    {
                      "location": {
                        "postalAddress": {
                          "regionCode": "US",
                          "recipients": [
                            "John Smith"
                          ],
                          "postalCode": "94043",
                          "locality": "MOUNTAIN VIEW",
                          "addressLines": [
                            "1600 AMPHITHEATRE PKWY"
                          ],
                          "administrativeArea": "CA"
                        }
                      },
                      "type": "DELIVERY"
                    }
                  ]
                },
                "totalPrice": {
                  "amount": {
                    "nanos": 760000000,
                    "units": "22",
                    "currencyCode": "USD"
                  },
                  "type": "ESTIMATE"
                },
                "id": "UNIQUE_ORDER_ID222",
                "cart": {
                  "lineItems": [
                    {
                      "quantity": 1,
                      "subLines": [
                        {
                          "note": "Note from the author"
                        }
                      ],
                      "price": {
                        "amount": {
                          "nanos": 990000000,
                          "units": "8",
                          "currencyCode": "USD"
                        },
                        "type": "ACTUAL"
                      },
                      "name": "My Memoirs",
                      "id": "mymemoirs_title",
                      "type": "REGULAR"
                    },
                    {
                      "quantity": 1,
                      "subLines": [
                        {
                          "note": "Special introduction by author"
                        }
                      ],
                      "price": {
                        "amount": {
                          "nanos": 990000000,
                          "units": "10",
                          "currencyCode": "USD"
                        },
                        "type": "ACTUAL"
                      },
                      "name": "Memoirs of a person",
                      "id": "memoirs_2",
                      "type": "REGULAR"
                    }
                  ],
                  "notes": "The Memoir collection",
                  "merchant": {
                    "name": "A Book Store",
                    "id": "book_store_1"
                  },
                  "otherItems": [
                    {
                      "price": {
                        "amount": {
                          "nanos": 980000000,
                          "units": "19",
                          "currencyCode": "USD"
                        },
                        "type": "ESTIMATE"
                      },
                      "name": "Subtotal",
                      "id": "subtotal",
                      "type": "SUBTOTAL"
                    },
                    {
                      "price": {
                        "amount": {
                          "nanos": 780000000,
                          "units": "2",
                          "currencyCode": "USD"
                        },
                        "type": "ESTIMATE"
                      },
                      "name": "Tax",
                      "id": "tax",
                      "type": "TAX"
                    }
                  ]
                }
              },
              "orderDate": "2018-12-19T20:43:39.073Z",
              "paymentInfo": {
                "displayName": "VISA-1234",
                "paymentType": "PAYMENT_CARD"
              },
              "googleOrderId": "02622302589300277076"
            }
          },
          "name": "TRANSACTION_DECISION_VALUE"
        }
      ]
    }
  ],
  "availableSurfaces": [
    {
      "capabilities": [
        {
          "name": "actions.capability.SCREEN_OUTPUT"
        },
        {
          "name": "actions.capability.AUDIO_OUTPUT"
        },
        {
          "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
        },
        {
          "name": "actions.capability.WEB_BROWSER"
        }
      ]
    }
  ]
}

4. Finalize 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 will then need a receipt object inside the OrderUpdate object with actionOrderId 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 have the option to pass a userVisibleOrderId. This can be the provided googleOrderId or your own custom identifier. Note that it will be displayed to the user in their receipt.

Part of the OrderUpdate object will need to contain an orderManagementActions object, which will manifest as URL buttons at the bottom of the order details that the user can find in their Assistant Order History.

If isInSandbox is true when the order is created, don't fulfill the order or charge the user (except via a payment sandbox in which only a test payment method is charged). We still recommend persisting the order in your own database, but be sure to flag it as a sandboxed order.

Fulfillment

Node.js
const arg = conv.arguments.get('TRANSACTION_DECISION_VALUE');
const finalOrderId = arg.order.finalOrder.id;
conv.ask(new OrderUpdate({
  actionOrderId: finalOrderId,
  orderState: {
    label: 'Order created',
    state: 'CREATED',
  },
  receipt: {
    userVisibleOrderId: '',
  },
  updateTime: new Date().toISOString(),
}));
conv.ask(`Transaction completed! You're all set! Would you like to do anything
else?'`);
Java
ResponseBuilder responseBuilder = getResponseBuilder(request);
OrderUpdate orderUpdate =
    new OrderUpdate()
        .setActionOrderId(googleOrderId)
        .setOrderState(new OrderState().setLabel("Order created").setState("CREATED"))
        .setReceipt(new Receipt().setConfirmedActionOrderId("123456789"))
        .setUpdateTime(Instant.now().toString());
responseBuilder
    .add("Transaction completed! You're all set! Would you like to do anything else?")
    .add(new StructuredResponse().setOrderUpdate(orderUpdate));
return responseBuilder.build();
Dialogflow JSON

Note that the JSON below describes a webhook response.

{
  "payload": {
    "google": {
      "expectUserResponse": true,
      "richResponse": {
        "items": [
          {
            "structuredResponse": {
              "orderUpdate": {
                "actionOrderId": "UNIQUE_ORDER_ID223",
                "orderState": {
                  "label": "Order created",
                  "state": "CREATED"
                },
                "receipt": {
                  "userVisibleOrderId": ""
                },
                "updateTime": "2019-01-09T23:25:44.307Z"
              }
            }
          },
          {
            "simpleResponse": {
              "textToSpeech": "Transaction completed! You're all set! Would you like to do anything\n      else?'"
            }
          }
        ]
      },
      "userStorage": "{\"data\":{}}"
    }
  },
  "outputContexts": [
    {
      "name": "/contexts/_actions_on_google",
      "lifespanCount": 99,
      "parameters": {
        "data": "{}"
      }
    }
  ]
}
Actions SDK JSON

Note that the JSON below describes a webhook response.

{
  "expectUserResponse": true,
  "expectedInputs": [
    {
      "inputPrompt": {
        "richInitialPrompt": {
          "items": [
            {
              "structuredResponse": {
                "orderUpdate": {
                  "actionOrderId": "UNIQUE_ORDER_ID223",
                  "orderState": {
                    "label": "Order created",
                    "state": "CREATED"
                  },
                  "receipt": {
                    "userVisibleOrderId": ""
                  },
                  "updateTime": "2019-01-09T23:25:44.316Z"
                }
              }
            },
            {
              "simpleResponse": {
                "textToSpeech": "Transaction completed! You're all set! Would you like to do anything\n      else?'"
              }
            }
          ]
        }
      },
      "possibleIntents": [
        {
          "intent": "actions.intent.TEXT"
        }
      ]
    }
  ],
  "conversationToken": "{\"data\":{}}",
  "userStorage": "{\"data\":{}}"
}

5. Send order updates

After the order has been created, we require that you send order updates throughout the life of the order. You provide order updates by sending an HTTP POST request to the Actions API with the order status and details.

Some important order updates will result in a push notification being sent to the user's Assistant-enabled mobile devices.

Order update details

After the user accepts the proposed order, you'll need to send an order update with a status of CREATED, along with a simple text response.

  • CREATED - Order is accepted by the user and "created" from the perspective of your Action but not yet confirmed; for example, if manual processing is required.

Over the course of the order's fulfillment, you'll also need to send the user updates with the following statuses:

  1. CONFIRMED - Order is confirmed - 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.

You can also use the following statuses if needed:

  • REJECTED - If you were unable to process, charge, or otherwise "activate" the order.
  • CANCELLED - Order was cancelled by the user.

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

Authorize requests to the Actions API

Order update requests to the Actions API are authorized by an access token. To POST an order update to the Actions API, you'll 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. From Google Cloud console > Menu ☰ > APIs & Services > Library > select Actions API > Enable
    • Make sure to select the correct Project ID from the dropdown
    • To find Project ID: Actions console then go to Settings
  2. Under Menu ☰ > APIs & Services > Credentials > Create credentials > Service account key.
  3. Under Service Account, select New Service Account.
  4. Set the service account to service-account.
  5. Set Role to Project > Owner.
  6. Set key type to JSON.
  7. Select Create.
  8. A private JSON service account key will be downloaded to your local machine.

In your order updates code, you can exchange your service key for a bearer token using the Google APIs client library and the "https://www.googleapis.com/auth/actions.fulfillment.conversation" scope. You can find installation steps and examples on the API client library GitHub page.

You can also reference order-update.js in our Node.js and Java samples for an example key exchange.

Send order updates

Once you've exchanged your service account key for an OAuth bearer token, you can send order updates as authorized POST requests to the Actions API.

Actions API URL: POST https://actions.googleapis.com/v2/conversations:send

The following headers should be provided:

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

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

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

The OrderUpdate object's top-level required fields include:

  • googleOrderId or actionOrderId - you may use the latter if you previously included a receipt containing a userVisibleOrderId.
  • 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 - a userNotification object with the title and text used for the update notification.
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);
  });
});

Java
// Create order update
String UNIQUE_ORDER_ID = "<UNIQUE_ORDER_ID>";
OrderUpdate orderUpdate = new OrderUpdate()
    .setActionOrderId(UNIQUE_ORDER_ID)
    .setOrderState(new OrderState()
        .setLabel("Order has been delivered!")
        .setState("FULFILLED"))
    .setUpdateTime(Instant.now().toString());

// Setup JSON body containing order update
JsonParser parser = new JsonParser();
JsonObject orderUpdateElement =
    parser.parse(new Gson().toJson(orderUpdate)).getAsJsonObject();
JsonObject orderUpdateJson = new JsonObject();
orderUpdateJson.add("order_update", orderUpdateElement);
JsonObject body = new JsonObject();
body.add("custom_push_message", orderUpdateJson);
body.addProperty("is_in_sandbox", true);
StringEntity entity = new StringEntity(body.toString());
entity.setContentType(ContentType.APPLICATION_JSON.getMimeType());
request.setEntity(entity);

// Make request
HttpClient httpClient = HttpClientBuilder.create().build();
HttpResponse response = httpClient.execute(request);

Troubleshooting

If you run into any issues during testing, you should read our troubleshooting steps for transactions.