Tutorial Scope
The guide below helps you build your desired Subscription Enrollment experience on your product detail pages (PDP) with Chargebee API.
Subscription Enrollment in e-commerce allows customers to subscribe to products or services directly from a store’s product detail page (PDP) or promotional product landing page. Chargebee's Subscription Enrollment functionality facilitates a seamless shopping experience customers, allowing them choose subscription frequency easily, view pricing information, understand savings, and decide between subscription or one-time purchase options.
Supported Subscription Models
Chargebee enables brands to build the following product subscription models listed below facilitate optimal revenue growth.
- Pay-Per-Delivery Subscriptions
- Pre-Pay Subscriptions
- Membership Subscriptions
- Gift Subscriptions
- Digital Subscription + Hardware Bundle (IoT)
Pay-Per-Delivery
The Pay-Per-Delivery model is utilized by brands that desire their customers to pay for their subscription before the product is sent to the customer. The pay-per-delivery model is often paired with customized promotions like subscribe-and-save discounts to encourage customers to purchase a subscription at a discount instead of purchasing the product as a one-time purchase.
Pre-Pay Subscriptions
The Pre-Pay model is utilized by brands that desire their customers to pay for their subscription upfront for a period (i.e., annually), and the customer gets a product delivery every month. The pre-pay model is often paired with customized promotions like subscribe-and-save discounts to encourage customers to purchase a subscription at a discount instead of purchasing the product as a one-time purchase.
Membership Subscriptions
The Membership model is utilized by brands that desire to launch a paid membership program for their brands—paid memberships programs often product the shopper with free shipping, members-only pricing, and cash back to encourage repeat business.
Gift Subscriptions
Gift Subscriptions are utilized by brands that desire to generate revenue by allowing your customers to gift subscriptions to family and friends. Gifts could be sent to recipients immediately on sign-up or scheduled for special occasions such as holidays or birthdays.
Digital Subscription + Hardware (One-time purchase) Bundles
Digital Subscription + Hardware bundles are often utilized by IoT brands selling hardware embedded with software, sensors, or other technologies paired with a digital service subscription with the purchase of keeping the hardware device connected and exchanging data with other devices or systems over the internet.
Feature Menu: Subscription Enrollment
The following is the list of feature menus for subscription enrollment:
- View and Select Product Variants
- View Product Purchasing Options
- Delivery Frequency Selector
- Product Variant Price per frequency
- Subscription Description (dynamic per product variant/plan selected)
- Call to Action button (For example, Add to Cart, Add Subscription, and Add Membership)
API Tutorial: Subscription Enrollment
Chargebee Setup
To set up Chargebee for Subscription Enrollment, here are the steps.
- Sign in or Sign up to your Chargebee account.
- Configure multi-currency pricing if required.
- Configure product taxes.
- Configure Customer or Subscription custom fields if required.
- Enable and Create product details, including product options and variants
- Create bulk Products / Variants in Chargebee.
- Use your third-party Variant Pricing with Chargebee.
- Create a plan, addon, or charge and adding an existing product.
- Enable Gift Subscriptions if required.
Feature Menu Implementation Guide using API
The following is an API implementation guide for building your desired Subscription Enrollment experience for your store.
PDP Widget Creation
A PDP (Product Detail Page) widget is a user interface element designed to showcase specific product information and options to customers on an e-commerce website. In this demo PDP widget, the focus is on providing options related to product variants, subscription frequency, quantity, and actions such as "Add to Cart" and "Subscribe Now." It allows customers to interact with the widget and choose their preferred options before making a purchase or subscribing to the product.
Here's a breakdown of the options available in the demo PDP widget:
- Product Variant: Customers can select from different product variants, such as size, color, or any other product attribute that offers variation. The dropdown button displays the available options, and customers can choose the variant that matches their preferences.
- Subscription Frequency with pricing: This dropdown button provides customers with options for different subscription frequencies with the respective price. The available choices are:
- One-Time Purchase: Customers can select this option if they want to make a one-time purchase without any recurring commitment.
- Yearly: Customers can choose to subscribe to the product and receive it on a yearly basis.
- Half-Yearly: Customers can opt for a subscription with deliveries scheduled every six months.
- Quarterly: This option allows customers to receive the product every three months through a subscription.
- Monthly: Customers can select this option to subscribe to a monthly delivery of the product.
- Quantity: Customers can input the desired quantity of the product they want to purchase or receive in each delivery if they choose a subscription frequency. The input field allows them to specify the exact quantity they need.
- Add to Cart: By clicking the "Add to Cart" button, customers can add the selected product variant and subscription preferences to their shopping cart for future checkout. This option is suitable for customers who prefer one-time purchases.
- Subscribe Now: The "Subscribe Now" button allows customers to proceed with subscribing to the product with the chosen subscription frequency and quantity. On clicking this button, the page will redirect to the Checkout flow. Once checkout is completed, the product will be added to their subscription list, and they will receive it based on the selected frequency.
This is an interactive widget that allows you to explore its real-time appearance and functionality.
Important
The above PDP demo is only an example. All elements in the demo is holding dummy data and does not request any Chargebee APIs. To create your own product details page please implement these frontend and backend codes.
Creating a PDP Widget
The following code sample is for the above PDP demo example:
Prerequisite
- Install Node js version 14 or above.
- Please note that the following instructions consider that you have the required API key and other necessary credentials to use the Chargebee APIs.
Steps to build the PDP widget
The following are the steps to build PDP widget using code snippets.
Frontend
Step 1: Build HTML for Widget
Create a new folder such as subscription-enrollment-example
, and create a new HTML file named cb-widget.html
and paste the following code snippet into it. You can modify the appearance of the widget by modifying the CSS variables.
<!DOCTYPE html>
<html>
<head>
<title>PDP Widget</title>
<style>
body {
min-height: 100vh;
}
/*
CSS Variables - Configure styling by modifying these variables
*/
.cb-widget-container {
--cb-font-family: system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
--cb-font-size: 14px;
--cb-font-color: #012a38;
--cb-border-radius: 4px;
--cb-box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px;
--cb-widget-bg: #fff;
--cb-cta-bg: #012a38;
--cb-cta-text: #fff;
--cb-variant-bg: #fff;
--cb-variant-active-bg: #137cb6;
--cb-variant-border: 1px solid #012a38;
}
.cb-loading-container {
text-align: center;
margin-top: 180px;
}
.cb-button-disabled {
pointer-events: none;
cursor: not-allowed;
opacity: 0.5;
}
.cb-widget-container {
position: fixed;
min-height: 400px;
width: 400px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 400px;
padding: 30px 20px;
border: 1px solid #ccc;
border-radius: var(--cb-border-radius);
font-family: var(--cb-font-family);
font-size: var(--cb-font-size);
color: var(--cb-font-color);
box-shadow: var(--cb-box-shadow);
background-color: var(--cb-widget-bg);
}
.cb-widget-title {
margin-top: 0;
}
.cb-variant-select-wrapper,
.cb-frequency-select,
.cb-frequency-select select,
.cb-quantity {
margin-bottom: 10px;
}
.cb-quantity {
padding-top: 20px;
}
.cb-quantity label {
margin-right: 20px;
}
.cb-quantity button {
border: none;
padding: 5px 10px;
}
.cb-quantity-wrapper {
display: inline-block;
border: 1px solid gray;
}
.cb-quantity-wrapper span {
margin: 0 15px;
}
.cb-frequency-select select,
.cb-variant-select {
display: block;
margin-top: 10px;
width: 100%;
height: 36px;
padding: 5px;
border-radius: var(--cb-border-radius);
}
.cb-cta-button {
padding: 10px 20px;
background-color: var(--cb-cta-bg);
color: var(--cb-cta-text);
border-radius: var(--cb-border-radius);
text-decoration: none;
display: block;
text-align: center;
margin-top: 10px;
}
.cb-variant-selector {
padding: 10px 0;
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
}
.cb-variant-selector button {
margin-top: 10px;
padding: 10px 16px;
border-radius: var(--cb-border-radius);
border: var(--cb-variant-border);
background: var(--cb-variant-bg);
cursor: pointer;
}
.cb-variant-selector button.active {
background: var(--cb-variant-active-bg);
color: #fff;
border: 0;
}
.cb-cta-container {
margin-top: 30px;
}
</style>
</head>
<body>
<!-- Widget Wrapper Container starts -->
<div class="cb-widget-container">
<!-- Loading Container starts -->
<div class="cb-loading-container" style="display: block">Loading...</div>
<!-- Loading Container ends -->
<!-- Widget Main Container starts -->
<div class="cb-widget-loaded" style="display: none">
<!-- Widget Title -->
<h2 class="cb-widget-title">PDP Widget</h2>
<!-- Variant Selector starts -->
<div class="cb-variant-select-wrapper">
<label for="cb-variant">Select Variant:</label>
<div class="cb-variant-selector">
<button class="cb-variant">Variant 1</button>
<button class="cb-variant">Variant 2</button>
<button class="cb-variant">Variant 3</button>
</div>
</div>
<!-- Variant Selector ends -->
<!-- Frequency Selector starts -->
<div class="cb-frequency-select">
<label for="cb-frequency">Choose Purchase Option:</label>
<select id="cb-frequency">
<option value="monthly">Monthly</option>
<option value="quarterly">Quarterly</option>
<option value="annually">Annually</option>
</select>
</div>
<!-- Frequency Selector ends -->
<!-- Quantity Selector starts -->
<div class="cb-quantity">
<label for="cb-quantity-input">Quantity:</label>
<div class="cb-quantity-wrapper">
<button type="button" class="cb-decrement-btn">-</button>
<span id="cb-quantity-input">1</span>
<button type="button" class="cb-increment-btn">+</button>
</div>
</div>
<!-- Quantity Selector ends -->
<div class="cb-subs-description"></div>
<!-- CTA Wrapper starts -->
<div class="cb-cta-container">
<!-- Add click event for Add to Cart to add your cart logic -->
<a class="cb-cta-button" id="cb-add-to-cart" href="javascript:void(0)"
>Add to Cart</a
>
<a class="cb-cta-button" id="cb-checkout" href="#">Subscribe Now</a>
</div>
<!-- CTA Wrapper ends -->
</div>
<!-- Widget Main Container ends -->
</div>
<!-- Widget Wrapper Container ends -->
<script src="/static/cb-widget.js"></script>
</body>
</html>
Step 2: Add Client-side functionality
The Initialization of Chargebee widget requires few properties as stated below:
Property | Description | Allowed values |
product_id * | Provide the product ID for which the widget should be displayed. This is a mandatory property. | Accepts: string |
currency * | Provide the Currency code of the store. Refer supported Currency codes by Chargebee. This is a mandatory property. | Accepts: string Default value: USD |
variantSelector | Determines how the variants should be displayed. It can either be displayed as group of Buttons or Select dropdown. This is an optional property. | Accepts: button /select Default value: select |
showAddToCart | Determines whether to display the Add to cart CTA. This is an optional property. | Accepts: true /false Default value: true |
showSubscribeNow | Determines whether to display the Subscribe Now CTA. This is an optional property. | Accepts: true /false Default value: true |
CbWidget.init({
product_id: 'PRODUCT_ID',
variantSelector: 'select',
currency: 'USD',
showAddToCart: true,
showSubscribeNow: true
})
Create a new javascript file named cb-widget.js
and paste the following code snippet into it. Modify the CbWidget.init
call with appropriate product_id
and currency
.
// CbWidget contains all the logic required to build the Widget
const CbWidget = {
inited: false,
options: {
site_id: '', // Mandatory field - Site ID
customer_id: '', // Optional
product_id: '', // Mandatory
variantSelector: 'select', // button/select
currency: 'USD', // Valid Currency Code
showAddToCart: true, // Display Add to Cart in your widget
showSubscribeNow: true // Display Subscribe now in your widget
},
variants: [],
quantity: 1,
prices: {
subscriptionPrices: [],
oneTimePrices: []
},
widgetData: {}, // Populate data to this property and use it to build widget
selectors: {
loadingContainer: document.getElementsByClassName(
'cb-loading-container'
)[0],
loadedContainer: document.getElementsByClassName('cb-widget-loaded')[0],
variantSelector: document.getElementsByClassName('cb-variant-selector')[0]
},
// Utility to make API call to your backend
fetchCBApi: async function (url, options = {}) {
const response = await fetch(url, options);
return response.json();
},
// initialize CbWidget. Entry point to get started
init: async function (options = {}) {
// Override default options with the custom options
this.options = {
...this.options,
...options
};
// Fetch APIs required to populate data in widgetData
await this.retrieveData();
// Use the data and options provided to construct the widget data
this.constructWidgetData();
// Using the widgetData render the widget on the page
this.renderWidget();
// Trigger click/change event on Variant selector to pre-select the first Variant by default
if (this.options.variantSelector === 'button') {
document.querySelector('.cb-variant-select-wrapper button').click();
} else {
document
.querySelector('.cb-variant-select')
.dispatchEvent(new Event('change'));
}
// Add event handlers for '-' and '+' buttons of Quantity selectors
document
.querySelector('.cb-decrement-btn')
.addEventListener('click', () => this.quantityModifier(-1));
document
.querySelector('.cb-increment-btn')
.addEventListener('click', () => this.quantityModifier(1));
// Show blocks based on the options provided
if (!this.options.showAddToCart) {
this.toggleBlock('#cb-add-to-cart', true);
}
if (!this.options.showSubscribeNow) {
this.toggleBlock('#cb-checkout', true);
} else {
// Add event handler for Subscribe now
document
.querySelector('#cb-checkout')
.addEventListener('click', (e) => this.subscribeNow(e));
}
},
retrieveData: async function () {
let error = null;
// Fetch Variant, Plans, Charges info
const [variants, subscriptionPlans, oneTimeCharges] = await Promise.all([
this.fetchCBApi('/api/variants?product_id=' + this.options.product_id),
this.fetchCBApi(
'/api/fetch-items?product_id=' + this.options.product_id + '&type=plan'
),
this.fetchCBApi(
'/api/fetch-items?product_id=' +
this.options.product_id +
'&type=charge'
)
]).catch((err) => {
// Show Error message on the widget
this.showError();
console.error(err);
error = err;
});
// Fetch Subscription price and one time prices
const [subscriptionPrices, oneTimePrices] = await Promise.all([
this.fetchCBApi('/api/fetch-item-prices?item_id=' + subscriptionPlans.id),
this.fetchCBApi('/api/fetch-item-prices?item_id=' + oneTimeCharges.id)
]).catch((err) => {
// Show Error message on the widget
this.showError();
console.log(err);
error = err;
});
if (error) {
return;
}
// Hide loading message and display the Widget content
this.inited = true;
this.selectors.loadingContainer.style.display = 'none';
this.selectors.loadedContainer.style.display = 'block';
// Populate the fetched data into the appropriate placeholders for easy reference
this.variants = variants.list;
this.prices.subscriptionPrices = subscriptionPrices;
this.prices.oneTimePrices = oneTimePrices;
},
constructWidgetData: function () {
this.variants.forEach((variant) => {
this.widgetData[variant.id] = {
...variant,
oneTimePrices: this.prices.oneTimePrices[variant.id],
subscriptionPrices: this.prices.subscriptionPrices[variant.id]
};
});
},
renderWidget: function () {
// Construct Variant Selector
this.selectors.variantSelector.innerHTML = '';
// Filter the variants that have atleast one price configured
const filteredVariants = Object.keys(this.widgetData).filter(
(variantId) => {
const variant = this.widgetData[variantId];
return (
variant.oneTimePrices?.length || variant.subscriptionPrices?.length
);
}
);
// If the variant selector is configured as Select, Create Select containers
if (this.options.variantSelector === 'select') {
const select = document.createElement('select');
select.classList = ['cb-variant-select'];
this.selectors.variantSelector.appendChild(select);
// Each Variants have unique set of Frequency prices, Change frequencies whenever Variant is changed
select.addEventListener('change', (e) => {
this.populateFrequencies(e);
});
}
filteredVariants.forEach((variantId, index) => {
const variant = this.widgetData[variantId];
// Create button containers if the variant selector is configured as Buttons
if (this.options.variantSelector === 'button') {
const button = document.createElement('button');
button.value = variant.id;
button.textContent = variant.name;
if (index === 0) {
button.className = 'active';
}
this.selectors.variantSelector.appendChild(button);
// Each Variants have unique set of Frequency prices, Change frequencies whenever Variant is changed
button.addEventListener('click', (e) => {
this.populateFrequencies(e);
});
} else {
const option = document.createElement('option');
option.value = variant.id;
option.textContent = variant.name;
document
.getElementsByClassName('cb-variant-select')[0]
.appendChild(option);
}
});
},
populateFrequencies: function (e) {
// Get the Variant Id
const variantId = e.target.value;
// Toggle the active class to highlight the selected Variant in the UI
if (this.options.variantSelector === 'button') {
document.querySelector('.cb-variant-select-wrapper .active').className =
'';
e.target.className = 'active';
}
const frequencySelector = document.querySelector('#cb-frequency');
let frequencies = [];
// Check if there are One time prices and populate them first
const oneTime = this.widgetData[variantId].oneTimePrices?.find((price) => {
return price.currency_code === this.options.currency;
});
if (oneTime) {
frequencies.push(oneTime);
}
// Check if there are Subscription prices and populate them
const subscriptions = this.widgetData[variantId].subscriptionPrices?.filter(
(price) => {
return price.currency_code === this.options.currency;
}
);
if (subscriptions?.length) {
frequencies = [...frequencies, ...subscriptions];
}
frequencySelector.innerHTML = '';
// After populating the prices and frequencies, build the Frequency selector
frequencies.forEach((frequency) => {
const option = document.createElement('option');
option.value = frequency.id;
option.textContent = `${frequency.period_unit || 'One Time'} - ${
frequency.currency_code
} ${(frequency.price / 100).toFixed(2)}`;
option.dataset.description = frequency.description || '';
frequencySelector.appendChild(option);
});
frequencySelector.addEventListener('change', (e) => {
this.changeFrequency(e);
});
document.querySelector('#cb-frequency').dispatchEvent(new Event('change'));
},
changeFrequency: function (e) {
this.widgetData.selectedFrequency = e.target.value;
const subsDescription = document.querySelector('.cb-subs-description');
subsDescription.innerHTML = '';
const desc = document.querySelector(
'#cb-frequency [value=' + e.target.value + ']'
)?.dataset?.description;
subsDescription.innerHTML = desc ? desc : '';
},
subscribeNow: async function (e) {
e.preventDefault();
try {
const checkout = await this.fetchCBApi(
`/api/generate_checkout_new_url?subscription_items[item_price_id][0]=${this.widgetData.selectedFrequency}&subscription_items[quantity][0]=${this.quantity}&customer[id]=${this.options.customer_id}`,
{
method: 'POST'
}
);
window.location.href = checkout.url;
} catch (e) {
console.error(e);
}
},
quantityModifier: function (count) {
if (this.quantity + count > 0) {
this.quantity = this.quantity + count;
document.querySelector('#cb-quantity-input').innerHTML = this.quantity;
}
},
toggleBlock: function (selector, isHide) {
if (!document.querySelector(selector)) {
return;
}
document.querySelector(selector).style = isHide
? 'display: none'
: 'display: block';
},
showError: function () {
this.selectors.loadingContainer.innerHTML =
'Sorry, Something went wrong. Try reloading the page.';
}
};
CbWidget.init({
site_id: 'SITE_ID',
customer_id: 'CUSTOMER_ID',
product_id: 'PRODUCT_ID',
variantSelector: 'select', // select/button
currency: 'USD' // 'USD', 'EUR', etc.,
});
Backend
Step 3: Create package.json for express server
Inside the subscription-enrollment-example
folder, create a new file named package.json
and paste the following code snippet into it:
{
"name": "subscription-enrollment-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"chargebee": "^2.6.0",
"cors": "^2.8.4",
"express": "^4.16.2",
"node-fetch": "^2.6.11"
}
}
Step 4: Install dependencies
Open a terminal, navigate to the ../subscription-enrollment-example
folder path, and run npm install
to install the dependencies.
Step 5: Create API routes to interact with Chargebee API
To connect your backend with Chargebee APIs, the following information is required:
Create a new javascript file named index.js
and paste the following code snippet into it. Add your store's site name and full access API key to this file.
Note:
- CORS (Cross-Origin Resource Sharing) is enabled only for demo purposes and is not recommended to use in the production server unless you understand CORS thoroughly and handle it appropriately. Remember to replace YOUR API KEY in the index.js file with your actual API key before running the server
- The code in GitHub has been modularized to enhance reusability. You have the option to utilize any of the references as per your requirement.
const express = require('express');
const chargebee = require('chargebee');
const path = require('path');
const fetch = require('node-fetch');
// CORS is enabled only for demo. Please dont use this in production unless you know about CORS
const cors = require('cors');
const siteName = 'SITE_ID'; // Add your site name here
const API_KEY = 'API_KEY'; // Add your Full access API key here
chargebee.configure({
site: siteName, // Enter your Side ID here
api_key: API_KEY // Enter your publishable API key here
});
const app = express();
app.use(express.urlencoded());
app.use(cors());
// Configure your static file paths here. Images, CSS and JS files should be inside this path
app.use('/static', express.static('/'));
/*
Fetch Item API
request params - Product ID, Item type - plan/charge
*/
app.get('/api/fetch-items', (req, res) => {
chargebee.item
.list({
limit: 1,
'status[is]': 'active',
'product_id[is]': req.query.product_id,
'type[is]': req.query.type
})
.request(function (error, result) {
if (error) {
//handle error
console.log(error);
} else {
var item;
if (result.list.length) {
item = result.list[0].item;
}
res.status(200).json(item);
}
});
});
/*
Fetch Item Price API
request params - Item ID
*/
app.get('/api/fetch-item-prices', (req, res) => {
let price = {};
chargebee.item_price
.list({
limit: 100,
'item_id[is]': req.query.item_id
})
.request(function (error, result) {
if (error) {
//handle error
console.log(error);
} else {
for (var i = 0; i < result.list.length; i++) {
var entry = result.list[i];
var item = entry.item_price;
if (price[item.variant_id]) {
price[item.variant_id].push(item);
} else {
price[item.variant_id] = [item];
}
}
res.status(200).json(price);
}
});
});
/*
Fetch Variants API
request params - Product ID
*/
app.get('/api/variants', async (req, res) => {
let variants = [];
const response = await fetch(
`https://${siteName}.chargebee.com/api/v2/products/${req.query.product_id}/variants`,
{
headers: {
Authorization: `Basic ${Buffer.from(API_KEY).toString('base64')}`
}
}
);
const varJson = await response.json();
for (let i = 0; i < varJson.list.length; i++) {
const variant = varJson.list[i].variant;
variants.push(variant);
}
res.status(200).json({ list: variants });
});
/*
Fetch Checkout Link
request params - Item Price ID, Customer ID (optional)
*/
app.post('/api/generate_checkout_new_url', async (req, res) => {
try {
chargebee.hosted_page
.checkout_new_for_items(req.query)
.request(function (error, result) {
if (error) {
//handle error
console.log(error);
} else {
res.send(result.hosted_page);
}
});
} catch (e) {
console.log(e);
}
});
// Configure the path of your HTML file to be loaded
app.get('/', (req, res) => {
res.sendFile(
path.join(__dirname, '/cb-widget.html')
);
});
app.listen(8000, () =>
console.log('subscription-enrollment-example listening on port 8000!')
);
Step 6: Start local server
Run node index.js
in your terminal to start the server.
Step 7: Check your widget
Access the widget at http://localhost:8000.
We're always happy to help you with any questions you might have!
support@chargebee.com