Code Deep Dive: Implementing In-Skill Purchasing for Entitlements with Node.js

Editor’s Note: In this new code deep dive series, we will provide an end-to-end walkthrough of how to implement in-skill purchasing (ISP) in your Alexa skill. We will be using the Premium Hello World Skill (available on GitHub), which is a sample skill that demonstrates how to use ISP features by offering a “Premium Greeting Pack” that greets the customer in a variety of languages like French, Spanish, Hindi, and more. We will explain each line of code as we walk through several scenarios that monetized skills should be able to handle. With each installment of this series, we’ll introduce you to a new ISP product, starting with entitlements (or one-time purchases) in this post. We will explore subscriptions and consumables in future posts. If you’d like, you can follow along by referencing the steps in the GitHub guide to set up the Premium Hello World skill on your developer account.

Today we will look at a few ISP-related scenarios and walk through how we are handling each step in our skill code. We’ll focus on four scenarios to give you an understanding of how the whole experience works.

Scenarios 1 and 2 illustrate how to clearly distinguish premium content from free content in your skill, and “upsell” an in-skill product to a customer.

Scenario 3 shows how to provide customers with an easy way to know what they have already purchased by supporting the “what did I buy” utterance, and how to allow customers to learn about your premium content on-demand by supporting a “what can I buy” utterance.

Finally, Scenario 4 brings it all together to illustrate the experience for a customer who has bought the premium product, and can access the premium content seamlessly.

Also, be sure to read the certification guidelines and marketing guidelines to ensure that you’ve covered all of the requirements.

Let’s get started.

Scenario 1: Customer has not purchased the in-skill product. Our skill should upsell the value of the Premium Greeting Pack.

In this scenario, we let the customer experience the standard greeting twice and then we offer them (upsell) a premium experience. In this happy path illustrated below, we see them make the purchase and receive the experience (product) they have unlocked. As you read through the storyboard, notice the example dialog in yellow/blue (user/Alexa) and the basic logic on the right.

The customer launches the skill (A) and does not currently have the premium pack. Our skill will respond back with the standard greeting in this first turn, and ask them if they would like to hear another greeting.

Next, the customer responds with a “Yes” to the prompt “Would you like another greeting?(B). Our skill now informs them that they can purchase the “Premium Greeting Pack”, which will give them access to greetings in other languages. We do this by prompting them with an “Upsell” offer, by sending a Connections.SendRequest directive to Alexa, with a name: 'Upsell' property. We do this inside our helper function getResponseBasedOnAccessType(), and then call it from the AnotherGreetingHandler through the callback function checkForProductAccess().

//Inside getResponseBasedOnAccessType() helper function
//Customer has not bought the Premium Product. Upsell should be made.

  if (isUpsellNeeded) {
    const upsellMessage = `You don't currently own the Premium Greeting pack. ${
      premiumProduct[0].summary
    }. ${getRandomLearnMorePrompt()}`;
    const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
    return handlerInput.responseBuilder
      .addDirective({
        type: "Connections.SendRequest",
        name: "Upsell",
        payload: {
          InSkillProduct: {
            productId: premiumProduct[0].productId
          },
          upsellMessage
        },
        token: JSON.stringify(sessionAttributes)
      })
      .getResponse();
  }

This generates the following JSON back that our skill sends back to Alexa:

{
    "body": {
        "version": "1.0",
        "response": {
            "directives": [
                {
                    "type": "Connections.SendRequest",
                    "name": "Upsell",
                    "payload": {
                        "InSkillProduct": {
                            "productId": "amzn1.adg.product.a3c807a6-ca7b-4862-adfa-eb92e268831c"
                        },
                        "upsellMessage": "You don't currently own the Premium Greeting pack. The Premium Greeting Pack greets you with a secret greeting. Want to learn about it?"
                    },
                    "token": "{}"
                }
            ],
            "type": "_DEFAULT_RESPONSE"
        },
        "sessionAttributes": {},
        "userAgent": "ask-node/2.3.0 Node/v8.10.0"
    }
}

 

//Inside AnotherGreetingWithUpsellHandler()
// IF THE USER SAYS YES, THEY WANT ANOTHER GREETING, AND UPSELL SHOULD BE MADE

const AnotherGreetingWithUpsellHandler = {
  canHandle(handlerInput) {
    return (
      handlerInput.requestEnvelope.request.type === "IntentRequest" &&
      handlerInput.requestEnvelope.request.intent.name === "AMAZON.YesIntent" &&
      handlerInput.attributesManager.getSessionAttributes().shouldUpsell ===
        true
    );
  },
  handle(handlerInput) {
    //Get the locale for the request
    const locale = handlerInput.requestEnvelope.request.locale;

    //Instantiate a new MonetizationServiceClient object to invoke the inSkillPurchaseAPI
    const monetizationClient = handlerInput.serviceClientFactory.getMonetizationServiceClient();

    //Check if upsell should be done. You can set your own upsell timing logic inside the shouldUpsell() function.
    const isUpsellNeeded = true;

    //Get list of products the customer has bought, and then respond accordingly
    return monetizationClient.getInSkillProducts(locale).then(function(result) {
      //Pass the handlerInput, list of products customer has access to (result), and the flag for upsell to the helper function checkForProductAccess to determine the response.
      let response = checkForProductAccess(
        handlerInput,
        result,
        isUpsellNeeded
      );
      //Modify the Upsell message so we hear a standard greeting before it
      const originalUpsellMessage =
        response.directives[0].payload.upsellMessage;
      const newUpsellMessage = `Here's your standard greeting: ${getStandardGreeting()}  ${originalUpsellMessage}`;
      // Setting a Session Attribute to keep track of the number of times the customer has said heard a standard greeting.
      // We will use this to determine if an upsell is required.
      const attributeNameToIncrement = `numberOfStandardGreetingsOfferedInThisSession`;
      incrementCountInSession(handlerInput, attributeNameToIncrement);
      response.directives[0].payload.upsellMessage = newUpsellMessage;
      return response;
    });
  }
};

 

//Inside checkForProductAccess() helper function

function checkForProductAccess(handlerInput, result, isUpsellNeeded) {
  const premiumProduct = result.inSkillProducts.filter(
    record => record.referenceName === `Premium_Greeting`
  );
  const response = getResponseBasedOnAccessType(
    handlerInput,
    premiumProduct,
    isUpsellNeeded
  );
  return response;
}

 

Handling the Purchase Experience Flow

At this point, Alexa’s Purchase Experience Flow takes over (C), and responds back to the customer with more details about the product (as provided by you when you created the product), along with the pricing information (again, as provided by you when you created the product). Amazon may even include a Prime discount for the customer.

“The Premium Greeting Pack greets you with a secret greeting. Prime members save $0.19. Without Prime, your price is $0.99 plus tax. Would you like to buy it?”

If the customer accepts the upsell offer (D) (by responding with a “Yes” to the prompt – “Would you like to buy it”?), Alexa responds back with a Connections.Response directive, which among other things, includes the payload.purchaseResult property, which indicates the result of the purchase transaction – ACCEPTED, DECLINED, ALREADY_PURCHASED, or ERROR.

If Customer Accepts the Upsell offer, our skill receives a purchaseResult of ACCEPTED. Like, so –

{
        "type": "Connections.Response",
        "requestId": "amzn1.echo-api.request.ace59d6a-2a7c-414c-b3d0-4a4b021d6fa4",
        "timestamp": "2019-02-20T23:58:07Z",
        "locale": "en-US",
        "status": {
            "code": "200",
            "message": "OK"
        },
        "name": "Upsell",
        "payload": {
            "purchaseResult": "ACCEPTED",
            "productId": "amzn1.adg.product.a3c807a6-ca7b-4862-adfa-eb92e268831c"
        },
        "token": "{}"
}

If Customer declines the Upsell offer, our skill receives a purchaseResult of DECLINED. Like, so –

{
        "type": "Connections.Response",
        "requestId": "amzn1.echo-api.request.674935ee-d248-4cdf-908b-fa04a9743d03",
        "timestamp": "2019-02-21T01:03:28Z",
        "locale": "en-US",
        "status": {
            "code": "200",
            "message": "OK"
        },
        "name": "Upsell",
        "payload": {
            "purchaseResult": "DECLINED",
            "productId": "amzn1.adg.product.a3c807a6-ca7b-4862-adfa-eb92e268831c",
            "message": "Skill Upsell was declined."
        },
        "token": "{}"
    }

It’s important to note that Amazon provides the purchase experience flow and also keeps track of which products are available and which have already been purchased. Your skill makes calls to the Monetization Service to determine if purchases have been made, and to pass purchase requests to Amazon.

Resuming the skill after Alexa’s Purchase Experience Flow is complete

As you can see in the JSON above, Alexa sends a Connections.Response directive back to our skill with a purchaseResult of “ACCEPTED” after the purchase is completed. We handle our response in the ConnectionsResponseHandler, as shown below.

//Inside ConnectionsResponseHandler.canHandle()    

 return (
      handlerInput.requestEnvelope.request.type === "Connections.Response" &&
      (handlerInput.requestEnvelope.request.name === "Buy" ||
        handlerInput.requestEnvelope.request.name === "Upsell")
    );
//Inside ConnectionsResponseHandler.handle()
//Check if the `purchaseResult` was ACCEPTED or DECLINED


switch (handlerInput.requestEnvelope.request.payload.purchaseResult) {
          case "ACCEPTED":
            theGreeting = getPremiumGreeting();
            speakOutput = `You have unlocked the Premium Greeting Pack. Here's your Premium greeting: ${
              theGreeting["greeting"]
            } ! That's hello in ${
              theGreeting["language"]
            }. ${getRandomYesNoQuestion()}`;
            const attributeNameToSave = `entitledProducts`;
            saveToSession(handlerInput, attributeNameToSave, premiumProduct);
            repromptOutput = getRandomYesNoQuestion();
            // resetting the count of standard greetings to avoid hitting upsell logic
            const secondAttributeNameToSave = `numberOfStandardGreetingsOfferedInThisSession`;
            const numberOfStandardGreetingsOfferedInThisSession = 1;
            saveToSession(
              handlerInput,
              secondAttributeNameToSave,
              numberOfStandardGreetingsOfferedInThisSession
            );
            break;
//Inside getPremiumGreeting() helper function

function getPremiumGreeting() {
  //TODO: Add more greetings
  const premium_greetings = [
    { language: "hindi", greeting: "Namaste" },
    { language: "french", greeting: "Bonjour" },
    { language: "spanish", greeting: "Hola" },
    { language: "japanese", greeting: "Konichiwa" },
    { language: "italian", greeting: "Ciao" }
  ];
  return premium_greetings[
    Math.floor(Math.random() * premium_greetings.length)
  ];
}

This generates the following speech output for the customer:

You have unlocked the Premium Greeting Pack. Here’s your Premium greeting: Bonjour ! That’s hello in french. Can I give you another greeting?

Scenario 2: Customer has not bought the product and asks for a premium greeting.

In this scenario, the customer does not currently have the “Premium Greeting Pack” and asks specifically for a premium greeting. In this case, our skill should make an upsell, and then hand off the control to Alexa to walk the customer through the Purchase Experience Flow, just as in the last scenario.

The utterances for getting a premium greeting will match the PremiumGreetingIntent, and trigger our PremiumGreetingHandler (B) as shown below.

As we did in the AnotherGreetingHandler in the last scenario, we will instantiate the Alexa Monetization Service Client, and call our checkForProductAccess function, which will then check if the customer has access to the product already. If not, it will launch the Purchase Experience Flow, just like in the last scenario (C & D above)

const PremiumGreetingHandler = {
  canHandle(handlerInput) {
    return (
      handlerInput.requestEnvelope.request.type === "IntentRequest" &&
      (handlerInput.requestEnvelope.request.intent.name ===
        "PremiumGreetingIntent" ||
        handlerInput.requestEnvelope.request.intent.name === "BuyIntent")
    );
  },
  handle(handlerInput) {
    const locale = handlerInput.requestEnvelope.request.locale;
    const monetizationClient = handlerInput.serviceClientFactory.getMonetizationServiceClient();
    const isUpsellNeeded = true;
    return monetizationClient.getInSkillProducts(locale).then(function(result) {
      return checkForProductAccess(handlerInput, result, isUpsellNeeded);
    });
  }
};

Scenario 3: Customer has not bought the product and asks “What have I bought?”

In this scenario, the customer wants to know what premium products (if any) they have bought.

Since the customer has not bought any products yet, we encourage them to ask about the products available to buy by saying – “what can I buy” (B).

The Utterance “what have I bought” is handled by the PurchaseHistoryHandler as shown below –

// User says: Alexa, ask Greetings helper what have I bought

const PurchaseHistoryHandler = {
  canHandle(handlerInput) {
    return (
      handlerInput.requestEnvelope.request.type === "IntentRequest" &&
      handlerInput.requestEnvelope.request.intent.name ===
        "PurchaseHistoryIntent"
    );
  },
  handle(handlerInput) {
    const locale = handlerInput.requestEnvelope.request.locale;
    const monetizationClient = handlerInput.serviceClientFactory.getMonetizationServiceClient();
    return monetizationClient.getInSkillProducts(locale).then(function(result) {
      const entitledProducts = getAllEntitledProducts(result.inSkillProducts);
      if (entitledProducts && entitledProducts.length > 0) {
        const speakOutput = `You have bought the following items: ${getSpeakableListOfProducts(
          entitledProducts
        )}. ${getRandomYesNoQuestion()}`;
        const repromptOutput = `You asked me for a what you've bought, here's a list ${getSpeakableListOfProducts(
          entitledProducts
        )}`;
        return handlerInput.responseBuilder
          .speak(speakOutput)
          .reprompt(repromptOutput)
          .getResponse();
      }
      const speakOutput = `You haven't purchased anything yet. Would you like a standard greeting or premium greeting`;
      const repromptOutput = `You asked me for a what you've bought, but you haven't purchased anything yet. You can say - what can I buy, or say yes to get another greeting. ${getRandomYesNoQuestion()}`;
      return handlerInput.responseBuilder
        .speak(speakOutput)
        .reprompt(repromptOutput)
        .getResponse();
    });
  }
};

Next, as advised by our skill, the customer asks “what can I buy”. This fires off the WhatCanIbuyIntent (C), and the skill responds with the list of products available to purchase. Our skill only has one premium product available to purchase – Premium Greeting Pack.

Here’s the handler – WhatCanIBuyHandler that generates that response.

//Inside WhatCanIBuyHandler// 
User says: Alexa, ask Greetings helper what can I buy

const WhatCanIBuyHandler = {
  canHandle(handlerInput) {
    return (
      handlerInput.requestEnvelope.request.type === "IntentRequest" &&
      handlerInput.requestEnvelope.request.intent.name === "WhatCanIBuyIntent"
    );
  },
  handle(handlerInput) {
    // Inform the user about what products are available for purchase
    const locale = handlerInput.requestEnvelope.request.locale;
    const monetizationClient = handlerInput.serviceClientFactory.getMonetizationServiceClient();
    return monetizationClient
      .getInSkillProducts(locale)
      .then(function fetchPurchasableProducts(result) {
        const purchasableProducts = result.inSkillProducts.filter(
          record =>
            record.entitled === "NOT_ENTITLED" &&
            record.purchasable === "PURCHASABLE"
        );
        if (purchasableProducts.length > 0) {
          const speakOutput = `Products available for purchase at this time are ${getSpeakableListOfProducts(
            purchasableProducts
          )}. To learn more about a product, say 'Tell me more about' followed by the product name. If you are ready to buy, say, 'Buy' followed by the product name. So what can I help you with?`;
          const repromptOutput = `I didn't catch that. What can I help you with?`;
          return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(repromptOutput)
            .getResponse();
        }
        const speakOutput = `There are no products to offer to you right now. Sorry about that. Would you like a greeting instead?`;
        const repromptOutput = `I didn't catch that. What can I help you with?`;
        return handlerInput.responseBuilder
          .speak(speakOutput)
          .reprompt(repromptOutput)
          .getResponse();
      });
  }
};

Moving ahead with out dialog, the customer now says “Tell me more about Premium Greeting”. This triggers the “PremiumGreetingIntent” (D).

At this point, Alexa’s Purchase Experience Flow takes over (E & F), and the dialog is similar to Scenario 2: Customer has not bought the product, and asks for a premium greeting , where the skill guides them through the purchase experience.

Scenario 4: Customer has purchased the in-skill product.

This is a pretty straight forward scenario. The customer has already purchased the Premium Greeting Pack, so our skill should respond back with a premium greeting, and ask them if they would like to hear another greeting, and respond back accordingly.

Here’s your Premium greeting: Konichiwa ! That’s hello in japanese. Would you like another greeting?

We handle the response for this in the AnotherGreetingHandler as shown below:

//Inside AnotherGreetingHandler//
//Utterance: Yes (in response to "do you want another greeting?")
// IF THE USER SAYS YES, THEY WANT ANOTHER GREETING.

const AnotherGreetingHandler = {
  canHandle(handlerInput) {
    return (
      handlerInput.requestEnvelope.request.type === "IntentRequest" &&
      handlerInput.requestEnvelope.request.intent.name === "AMAZON.YesIntent"
    );
  },
  handle(handlerInput) {
    //Get the locale for the request
    const locale = handlerInput.requestEnvelope.request.locale;

    //Instantiate a new MonetizationServiceClient object to invoke the inSkillPurchaseAPI
    const monetizationClient = handlerInput.serviceClientFactory.getMonetizationServiceClient();

    //Check if upsell should be done. You can set your own upsell timing logic inside the shouldUpsell() function.
    const isUpsellNeeded = false;

    //Get list of products the customer has bought, and then respond accordingly
    return monetizationClient.getInSkillProducts(locale).then(function(result) {
      //Pass the handlerInput, list of products customer has access to (result), and the flag for upsell to the helper function checkForProductAccess to determine the response.
      let response = checkForProductAccess(
        handlerInput,
        result,
        isUpsellNeeded
      );
      return response;
    });
  }
};

We hope you find this new series helpful as you embark on the journey to use ISP to sell premium content in US skills to enrich your Alexa skill experience and further delight your customers. You can reach out to me on Twitter @amit and my co-author for this series @memodoring. We can’t wait to see what you build!

Related Content

Check out these additional resources for more guides and best practices to consider when building a monetized skill: