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.
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()} <break/> ${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;
}
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.
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?
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);
});
}
};
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.
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!
Check out these additional resources for more guides and best practices to consider when building a monetized skill: