Create membership-based subscriptions with Shopify Checkout

In a membership model, a subscriber pays a monthly or annual fee to join an exclusive membership program. Merchants can then customize membership perks based on their business model. Build customer loyalty for your business by offering benefits like monthly store credit, credit back for additional purchases, exclusive discounts, early access to new releases, free gifts and more.

This tutorial outlines creating a member-based subscription using Shopify Checkout and Recharge. You will add a custom property to the Shopify customer object flagging whether or not a customer is a member of the loyalty program. You can then show or hide member benefits conditionality on the front end, depending on whether the customer object has this property.

Prerequisites

  • You will need a Shopify Access Token to make API calls to your Shopify store. See How to Generate a Shopify Access token
  • A Shopify Plus subscription
  • This is an example implementation using the Shopify Debut Theme and Smile.io. While many themes are similar, the exact file names and location of code blocks could differ depending on your chosen theme. Recharge also does not currently support credit system tracking out of the box. For the purposes of this tutorial you will need to install Smile.io.

📘

Note

Recharge does add an Active Subscriber tag to the Shopify customer account upon purchasing a subscription product, but we suggest creating your own unique customer tag to determine member and non-member status. Doing so gives you full control over the logic of adding and removing the customer.

Create server

You will need to create a server environment and execute two main functions. The first function will add the subscriber tag to the Shopify customer object under the tags property (this example uses the value StoreMember) and one to remove it. You will need to use Recharge’s API Webhooks to trigger these functions when necessary. This example demonstrates this process using serverless functions on Google Cloud Platform, but you can use any server environment to achieve this use case.

Register webhooks

You will need to register the following Recharge webhooks:

  • subscription/created
  • subscription/activated

The JavaScript script below provides an example for registering these webhooks:

📘

Note

This example uses the HTTP library for Node.js, but you can use any library that lets you make HTTP requests, such as Node Fetch.

require('request')
var request = require('request-promise');

var rc = {
  //ReCharge API Token
  rechargeApiToken: ‘<ReCharge API Token>’,

  createWebhook: function(address, topic) {
    return request({
      "method": "POST",
      "uri": "https://api.rechargeapps.com/webhooks",
      "headers": {
        "X-ReCharge-Access-Token": rc.rechargeApiToken,
        "Accept": "application/json",
        "Content-Type": "application/json"
      },
      "body": JSON.stringify({
        "address": address,
        "topic": topic,
      })
    });
  },
}

Add StoreMember tag to Shopify Customer

This function is triggered by the callback from a Recharge webhook. It takes in the Recharge customer ID and matches it to the Shopify Customer, then adds the loyalty member tag.

First, call the Recharge Customer API endpoint to obtain the customer’s Shopify Customer ID. This ID is what you will use to identify which customer in Shopify with receive the loyalty tag.

Next, call the Shopify API with the Shopify Customer ID as a parameter. This lets you obtain the customer’s current customer tags.

Parse the customer tags into a list. Shopify does not have a way to append a tag an item to the tags, so the code below demonstrates sorting the tags into a list. Once you have the tags list, check whether the StoreMember tag already exists. If it doesn’t, add the StoreMember tag to the list and then write the tags back to a string form.

Call the Shopify API again to update the customer’s tags with the new string list you have created.

Finally, exit the function and return a 200 response to the webhook.

require('request')
var request = require('request-promise');
var rc = {
  //ReCharge API Token
  rechargeApiToken: ‘<ReCharge API Token>’,
  //Shopify API Tokens
  ShopifyApiUsername: ‘<Shopify API Token>,
  ShopifyApiPassword: ‘<Shopify API Token Password>’,
  ShopifyApiShop: '<Shopify Store Name>',
  ShopifyApiVersion: '<Shopify API Version>’,

  ShopifyCustomerID: null,
  tagName: 'StoreMember',

  buildShopifyURL: function(resource) {
    return 'https://' + rc.ShopifyApiUsername + ':' + rc.ShopifyApiPassword + '@' + rc.ShopifyApiShop + '.myShopify.com/admin/api/' + rc.ShopifyApiVersion + '/' + resource;
  },

  getReChargeCustomer: function(customerID) {
    return request({
      "method": "GET",
      "uri": 'https://api.rechargeapps.com/customers/' + customerID,
      "headers": {
        "X-ReCharge-Access-Token": rc.rechargeApiToken
      }
    });
  },

  getShopifyCustomer: function(rcCustomer) {
    rc.ShopifyCustomerID = JSON.parse(rcCustomer).customer.shopify_customer_id;

    return request({
      "method": "GET",
      "uri": rc.buildShopifyURL('customers/' + rc.ShopifyCustomerID + '.json'),
    });
  },

  setCustomerTags: function(ShopifyCustomer) {
    var tagsString = JSON.parse(ShopifyCustomer).customer.tags;
    var tags = tagsString.split(",").map(function(item) {
      return item.trim();
    });

    return tags;
  },

  hasTag: function( arr, value) {
    return arr.indexOf(value) > -1;
  },

  updateShopifyCustomer: function(tagsArray) {
    return request({
      "method": "PUT",
      "uri": rc.buildShopifyURL('customers/' + rc.ShopifyCustomerID + '.json'),
      "json": true,
      "body": {
        "customer": {
          "tags": tagsArray.join()
        }
      },
    })
    .then(function(response) {
      return 'Shopify Customer Updated';
    });
  },

  processRequest: function(tags) {
    if(rc.hasTag(tags, rc.tagName)) {
      return rc.tagName + ' Tag Already Present';
    }
    else {
      tags.push(rc.tagName);
      console.log(rc.tagName + ' Tag Added');
      return rc.updateShopifyCustomer(tags);
    }
  }
}

exports.main = (req, res) => {
  var customerID = req.body.subscription.customer_id || null; // obtain the customer.id from the customer/created webhook payload

  if(customerID) {
    var result = rc.getReChargeCustomer(customerID)
      .then(rc.getShopifyCustomer)
      .then(rc.setCustomerTags)
      .then(rc.processRequest);
      
    console.log(result);
  }
  else {
    console.log("CustomerID not found.  Could not process webhook.")
  }
  res.status(200).send("Webhook Received");
};

Create function to remove StoreMember tag

You'll also need a function to remove the StoreMember tag from customers.

Within the processRequest function, find the index of the StoreMember and remove it from the list. Continue on with the function as described in the step above and return a 200 response to the webhook.

require('request')
var request = require('request-promise');
var rc = {
  //ReCharge API Token
  rechargeApiToken: ‘<ReCharge API Token>’,
  //Shopify API Tokens
  ShopifyApiUsername: ‘<Shopify API Token>,
  ShopifyApiPassword: ‘<Shopify API Token Password>’,
  ShopifyApiShop: '<Shopify Store Name>',
  ShopifyApiVersion: '<Shopify API Version>’,

  //Shopify Customer ID
  ShopifyCustomerID: null,
  tagName: 'StoreMember',

  buildShopifyURL: function(resource) {
    return 'https://' + rc.ShopifyApiUsername + ':' + rc.ShopifyApiPassword + '@' + rc.ShopifyApiShop + '.myShopify.com/admin/api/' + rc.ShopifyApiVersion + '/' + resource;
  },

  getReChargeCustomer: function(customerID) {
    return request({
      "method": "GET",
      "uri": 'https://api.rechargeapps.com/customers/' + customerID,
      "headers": {
        "X-ReCharge-Access-Token": rc.rechargeApiToken
      }
    });
  },

  getShopifyCustomer: function(rcCustomer) {
    rc.ShopifyCustomerID = JSON.parse(rcCustomer).customer.shopify_customer_id;

    return request({
      "method": "GET",
      "uri": rc.buildShopifyURL('customers/' + rc.ShopifyCustomerID + '.json'),
    });
  },

  setCustomerTags: function(ShopifyCustomer) {
    var tagsString = JSON.parse(ShopifyCustomer).customer.tags;
    var tags = tagsString.split(",").map(function(item) {
      return item.trim();
    });

    return tags;
  },

  hasTag: function( arr, value) {
    return arr.indexOf(value) > -1;
  },

  updateShopifyCustomer: function(tagsArray) {
    return request({
      "method": "PUT",
      "uri": rc.buildShopifyURL('customers/' + rc.ShopifyCustomerID + '.json'),
      "json": true,
      "body": {
        "customer": {
          "tags": tagsArray.join()
        }
      },
    })
    .then(function(response) {
      return 'Shopify Customer Updated';
    });
  },

  processRequest: function(tags) {
    if(rc.hasTag(tags, rc.tagName)) {
      var index = tags.indexOf(rc.tagName);
      if(index > -1) {
        tags.splice(index, 1);
      }
      console.log(rc.tagName + ' Tag Removed');
      return rc.updateShopifyCustomer(tags);
    }
    else {
      return rc.tagName + ' Tag Not Present';
    }
  }
}

exports.main = (req, res) => {
  let customerID = req.body.subscription.customer_id || null;
  if(customerID) {
    let result = rc.getReChargeCustomer(customerID)
      .then(rc.getShopifyCustomer)
      .then(rc.setCustomerTags)
      .then(rc.processRequest);
    console.log(result);
  }
  else {
    console.log("CustomerID not found.  Could not process webhook.")
  }
  res.status(200).send("Webhook Received");
};

Final code

The JavaScript below shows what the main .js file in your directory should look like. To summarize, your application does the following:

  • Creates webhooks
  • Invokes functions above
  • Sends 200 to Recharge webhook service
require('request')
var request = require('request-promise');

var rc = {
  //ReCharge API Token
  rechargeApiToken: ‘<ReCharge API Token>’,

  createWebhook: function(address, topic) {
    return request({
      "method": "POST",
      "uri": "https://api.rechargeapps.com/webhooks",
      "headers": {
        "X-ReCharge-Access-Token": rc.rechargeApiToken,
        "Accept": "application/json",
        "Content-Type": "application/json"
      },
      "body": JSON.stringify({
        "address": address,
        "topic": topic,
      })
    });
  },
}

function main() {

  var addTagAddress = '<Add Tag Function Address>';
  var removeTagAddress = '<Remove Tag Function Address>';

  rc.createWebhook(addTagAddress, 'subscription/created')
    .then(rc.createWebhook(addTagAddress, 'subscription/activated'))
    .then(rc.createWebhook(removeTagAddress, 'subscription/cancelled'))
    .then(rc.createWebhook(removeTagAddress, 'subscription/deleted'))
}

main();

Update Shopify products/Liquid code

Now that you can identify customers as members, you'll update the Shopify front end to reflect this new feature.

First, you will update the products and collections in Shopify. Then, you will update the associated Liquid code to show/hide the member benefits.

Add tags to products

Member-based purchasing relies on adding specific tags tied to each product. This allows you to segment products and create collections tied to either members or non-members. See Creating and using tags in Shopify for more information.

To do this, navigate to Products within the Shopify dashboard and add one of the following tags to each product based on the type of product it is:

Membership
Includes all Recharge subscription products used to purchase membership. Adding this tag lets you segment Recharge products into their own product page and prevent customers from redeeming their loyalty points on membership products.

Members-only
Includes all products that only members should have access to. This tag segments specific products into their own members only product page the general public will not have access to. Using this tag on products lets you create a pre-sale page or other similar functionality that gives members special access.

All-products
All other products that do not belong to membership or members-only. Due to a Shopify limitation, there is no way to create a collection excluding Membership or Members-Only. You must create a third tag when creating the all products collection.

👍

Note

The specific names for these tags can differ to your liking. You can also add more tags if you want to create different types of member sections. The important part is that the tags match consistently throughout all functionality.

Create collections

Navigate to Collections in the Shopify dashboard and create three new collections for the tags you created. Set the collections up as Automated collection with the following conditions for each:

  • Membership: Product tag is equal to membership
  • Members-Only: Product tag is equal to members-only
  • All-Products: Product tag is equal to all-products

See Collections to learn more about Shopify collections.

Add member specific code snippets

You'll modify the Shopify theme to check if customers have membership access and allow them to see member products. You'll add several reusable code snippets to your Shopify theme do this.

Open the Shopify Theme Editor by navigating to Online Store > Themes > Actions > Edit Code.

🚧

Note

We strongly encouraged you to duplicate your theme as a backup before making code changes. See Duplicating themes for details.

Add a new snippet called membership-check.liquid. This snippet should create two boolean variables you will use throughout the other portions of the code. Declare an isMember variable that will contain true if the user is a member, or false otherwise. Add another variable, isNotMember, containing true if the user is not a member, or false otherwise. To assign these variables, create a check for whether there is an active customer logged in, and that they have the StoreMember tag.

{% assign isMember = false %}
{% assign isNotMember = true %}
{% if customer and customer.tags contains "StoreMember" %}
    {% assign isMember = true %}
    {% assign isNotMember = false %}
{% endif %}

Add another new snippet called membership-access.liquid. This snippet contains the content your store displays when a non-member is alerted that they do not have access to an area of the store (i.e., a non-member navigates to a members only URL, a product is reserved exclusively for members). For this example the snipper below displays HTML directing the user to either log in or sign up for a membership. You can customize the snippet or create several as needed for your specific use case.

<h2 style="text-align:center;">Members Access Only</h2>
<p style="text-align:center;">
 <a href="/account/login">Log in</a> or <a href="/collections/membership">Sign Up Today</a>!
</p>

Update site navigation and routing

You will need to display different store navigation to members and non-members. To do this, you will create two new menu items in the store. Navigate to Online Store > Navigation within the Shopify dashboard and create the new items. For this example, add the following items:

  • Members-Menu: Home, All Products collection, Membership collection
  • Non-Members-Menu: Home, All Products collection, Members-Only collection
11521152

Navigate to the Shopify Theme Editor and open site-nav.liquid. At the top of the file add a conditional statement that checks whether the user is a member. Store the result in the menu_handle variable. Add an include statement that pulls in the membership-check.liquid file so you have access to the isMember variable. Once you have the correct value stored in menu_handle, update the for loop to use the menu_handle variable instead of section.settings.main_linklist inside the linklists object.

{% include 'membership-check' %}
{% assign menu_handle = "Non-Members-Menu" %}
{% if isMember %}
 {% assign menu_handle = "Members-Menu" %}
{% endif %}

<ul class="site-nav list--inline {{ nav_alignment }}" id="SiteNav">
  {% for link in linklists[menu_handle].links %}
    {%- assign child_list_handle = link.title | handleize -%}

Routing customers to correct collection

You have already set up a members only collection and limited the navigation to show that collection when the member is logged in, but there is nothing preventing a non-member from directly entering the URL of the members only page into their browser. You'll need to update your routing files to show the membership-access.liquid snippet instead of the member’s content when a non-member is trying to access it.

In the Shopify Theme Files open theme.liquid and navigate down to the {{ content_for_layout }} object (around line 131). Add an if statement to check whether the user is a member and whether they are trying to access a members only area.

If the user is not a member, redirect them to the membership-access.liquid snippet, otherwise show them the content designed for members.

<div class="page-container" id="PageContainer">
  <main class="main-content js-focus-hidden" id="MainContent" role="main" tabindex="-1">
    {% include 'membership-check' %}
    {% if isNotMember and handle contains "members-only" %}
      {% include 'membership-access' %}
    {% else %}
      {{ content_for_layout }}
    {% endif %}
  </main>

  {% section 'footer' %}

Update add to cart button for member only products

You might want to have general products that aren’t in the members only collection, but require the user to become a member to purchase them. To do this you can update the add to cart button to conditionally show them based on whether or not the user is a member.

The first step is to add a tag to all products that fit this criteria. In this example, add a tag called “Member-Product”.

Next, open the product-template.liquid template and navigate to the product form section (around line 180) and the add to cart button. Add an if statement to check whether the user is not a member and the product contains the Member-Product tag you just created. If so, display the membership-access snippet, otherwise display the add to cart button as normal.

{% include "membership-check" %}
{% if isNotMember and product.tags contains "Member-Product" %}
  {% include "membership-access" %}
{% else %}
  <div class="product-form__error-message-wrapper product-form__error-message-wrapper--hidden{% if section.settings.enable_payment_button %} product-form__error-message-wrapper--has-payment-button{% endif %}" data-error-message-wrapper role="alert">
    <span class="visually-hidden">{{ 'general.accessibility.error' | t }} </span>
    {% include 'icon-error' %}
    <span class="product-form__error-message" data-error-message>{{ 'products.product.quantity_minimum_message' | t }}</span>
  </div>

Update product pricing

You can set up your store to give discounted pricing site-wide to all products based on the user being a member. You will update the product templates that display the price on all product pages, then update the cart to pass the member price along to the cart. In this example, you will change your pricing to offer a 10% discount to all members.

Update product pages

All product pricing displayed on the site outside of the cart is derived from one liquid snippet. You will be update the pricing to show the full price if the user is not a member, or show a discounted price with a strikethrough full price next to it if the user is a member.

Open the product-price.liquid snippet. First, you will include the membership-check.liquid template to gain access the isMember variable. Then you will replace the if statement for the price--on-sale class in the dl tag to check the state of isMember. You do this to give the strikethrough pricing on the normal price and the highlighted color on the discounted pricing.

You will update the money_price variable to be called normal_price and add a membership_price variable. This membership price variable will contain the new price with 10% off.

<!-- snippet/product-price.liquid -->
{% include 'membership-check' %}

{% if variant.title %}
  {%- assign compare_at_price = variant.compare_at_price -%}
  {%- assign price = variant.price -%}
  {%- assign available = variant.available -%}
{% else %}
  {%- assign compare_at_price = 1999 -%}
  {%- assign price = 1999 -%}
  {%- assign available = true -%}
{% endif %}

{%- assign normal_price = price | money -%}
{%- assign membership_price = price | times: 0.9 | money -%}

<dl class="price{% if isMember %} price--on-sale{% endif %}{% if available and variant.unit_price_measurement %} price--unit-available{% endif %}" data-price>

Create cart script

To finalize creating strikethrough discounts for members on the storefront, you need will update the cart to reflect the new pricing. To do this you will add a custom script to the theme. The script will dynamically update the cart pricing as the user takes different actions (i.e., if the user adds all items to cart while logged out, the script will update the cart item’s prices once the member logs in). You will use the Shopify Scripts editor to do this. See Create a script for details.

Within Shopify Scripts editor navigate to Create script > Line items > Percentage (%) off a product, and name it 10% off member pricing. This script uses Ruby and will emulate similar functionality as found within the product-price.liquid snippet. Check to ensure the customer has a StoreMember tag, then loop through each line item in the cart and lower the price to 10% off for each, exempting non-membership products.

customer = Input.cart.customer

if !customer.nil? and customer.tags.include? "StoreMember"
  Input.cart.line_items.each do |line_item|
    product = line_item.variant.product

    next if product.tags.include? "Membership"
    line_item.change_line_price(line_item.line_price * 0.90, message: "Member Pricing")
  end
end

Output.cart = Input.cart

Update search capabilities

The next step in adding membership to the store is updating the the search. If a non-member is searching for products, you do not want member only items to appear.

Open search.liquid and navigate down to the search.results object loop (around line 58). In this for loop add an unless statement to skip all search results where the product contains the membership tag. This will remove Recharge subscription products from the search. Adding this unless statement also removes any products that are tagged as Members-Onlywhen the user is not logged in as a member. This prevents non-members from seeing the Members-Only products when searching.

🚧

Note

This functionality will filter results out of the loop of products to be displayed in the main body of search results. Updating the search in this manner will not affect pagination or any other areas of the page that use the product count from the search results. It's up to you to handle other areas of the search object.

<ul class="page-width list-view-items">
  {% include 'membership-check' %}
  {% for item in search.results %}
    {% unless item.tags contains "Membership" or isNotMember and item.tags contains "Members-Only" %}
      <li class="list-view-item">
        {% if item.object_type == 'product' %}
          {% include 'product-card-list', product: item %}
        {% else %}

Offer free shipping to members

You can offer members free shipping as another benefit of your membership program. Depending on your store settings, you can configure member pricing to offer free pricing on all shipping rates, or you can segment the shipping rates into different groups. For example, free standard shipping for being a member, but regular shipping pricing for expedited.

As shipping is calculated during the checkout process, you will need to create a new script to change it accordingly. Like the cart pricing discounts section above, you will need the Shopify Script Editor.

Navigate to Create script > Shipping rates > Modify shipping rate price, and name it Free Standard Shipping for Members. First, this script checks whether the customer has a StoreMember tag, then loops through each shipping rate. If the shipping rate is in your ELIGIBLE_SERVICES array, lower that shipping rate to $0.00. In this case, you are offering free shipping for members on all standard shipping, but normal pricing on all other shipping rates.

customer = Input.cart.customer
ELIGIBLE_SERVICES = ['Standard']

if !customer.nil? and customer.tags.include? "StoreMember"
  Input.shipping_rates.each do |shipping_rate|
    if ELIGIBLE_SERVICES.include? shipping_rate.name
      shipping_rate.apply_discount(shipping_rate.price, message: "Free shipping for Members!")
    end
  end
end

Output.shipping_rates = Input.shipping_rates 

Integrate membership points tracking with Smile.io

The final portion of this tutorial focuses on adding a membership points tracking system for customers. To make sure only members are receiving loyalty benefits, set up Smile actions that take effect only if the customer has a StoreMember tag.

  • Create a new Action when a customer places an order
  • Under Earning Type select increments of points (recommended)
  • Below Earning Value add 1
  • Under Customer Eligibility select Limit to customers based on specific tags
  • In the Customer tags to include field add the StoreMember tag
628628

Update theme portal to show customer points balance

Using the Recharge Theme Engine you will customize the Customer Portal so customers can manage their points balance. Smile.io provides a snippet that pulls this information into the Recharge theme.

{% include "membership-check" %}
{% if customer and isMember %}
  <div>{{ customer.first_name }} (Credits: <span class="sweettooth-points-balance"></span>)</div>
{% endif %}
10681068

Use Smile.io API to create points upon subscription renewal

A popular membership retention strategy is to award rewards points to members for resubscribing each month. Smile does not have a direct action to credit points on a recurring basis, thus adding this functionality requires using the Smile.io API.

You will need a server environment to listen a Recharge webhook. In this example, you will setup a serverless function similar to the functions you created for adding/removing the StoreMember tag.

Call the Smile API to retrieve the Smile customer. Pass the customer's email to obtain this info. Then use the Smile Customer ID from the response body and use it to credit the customer a certain amount of points.

This function should fire upon receiving the order/processed webhook. Every time the subscription is renewed for the customer, this function will trigger and credit the customer with more points.

require('request')
var request = require('request-promise');

var rc = {
  smileApiToken: '<Smile API Token>’,

  pointsIncrease: 10,
  pointsDescription: "Membership Renewal Bonus!",
  pointsInternalNote: "Membership Renewal Bonus!",

  getSmileCustomer: function(email) {
    return request({
      "method": "GET",
      "uri": 'https://api.smile.io/v1/customers/',
      "headers": {
        "Authorization": rc.smileApiToken,
        "Content-Type": "application/json"
      },
      "body": JSON.stringify({
        "email": email
      })
    });
  },

  updateSmilePointsBalance: function(response) {
    var customers = JSON.parse(response).customers;

    var customerid = null;
    for(var i=0; i<customers.length; i++) {
      customerid = customers[i].id;
    }

    return request({
      "method": "POST",
      "uri": 'https://api.smile.io/v1/points_transactions',
      "headers": {
        "Authorization": rc.smileApiToken,
        "Content-Type": "application/json"
      },
      "body": JSON.stringify({
        "points_transaction": {
          "customer_id": customerid,
          "points_change": rc.pointsIncrease,
          "description": rc.pointsDescription,
          "internal_note": rc.pointsInternalNote
        }
      })
    });
  },
}

exports.main = (req, res) => {
  var email = req.body.order.email || null;
  if(email) {
    var result = rc.getSmileCustomer(email)
      .then(rc.updateSmilePointsBalance)
      .then(function(response) {
        return "Points Balance Updated for " + email;
      });
      
    console.log(result);
  }
  else {
    console.log("Email not found.  Could not process webhook.")
  }
  res.status(200).send("Webhook Received");
};

Add points slider on checkout

As a final improvement to the user experience, you can follow Smile.io's tutorial, Points slider at checkout to allow customers to redeem points at checkout and encourage them to participate in the rewards program.


Need Help? Contact Us