Checking your Shopify customers for charge renewals on a recurring schedule.

If you've ever scoped out or planned a complex web application, such as a Shopify app, there's a good chance that you've wanted to use some sort of scheduled task that could fire every X hours. There are a ton of reasons you might want a recurring task in any web app, not just the Shopify flavor:

  • You might want to check the status of each user's subscription, then send an email whenever it renews for another month.
  • You could compile and send daily reports for your users, based on their usage.
  • You might need to do some data crunching and figure out how many orders the user has received that day.

When it comes to web apps, these recurring tasks are called "cron jobs", and they're an extremely common feature of almost every other web framework — except ASP.NET. Developers can easily schedule a task to fire and do some heavy lifting in e.g. 4 hours, or they can create a recurring task to fire every 24 hours.

Until recently, though, it's been practically impossible to do something on a recurring schedule in an ASP.NET application. Due to it's stateless nature, an ASP.NET app will recycle and shut itself down if it doesn't receive any requests for a short period of time. That recycling is what makes any kind of scheduled task completely fail, get deleted and never come back.

Some libraries, such as Quartz will let you create scheduled tasks in a .NET application, but they aren't meant for ASP.NET apps. They're still susceptible to the app recycling and shutting down.

That's a pickle for us ASP.NET devs, because background and scheduled jobs can be ideal for certain tasks. Luckily, Microsoft has finally given us access to recurring jobs and done something really special with their new Azure logic apps, letting us finally take advantage of something that other developers take for granted.

Logic apps are advanced, souped-up cron jobs. Not only can they do simple things like make a request to a URL every X hours, but they can chain actions together, passing the results of one action as the input to the next. They can be triggered by events much more advanced than a simple timer, such as firing when a record in your SQL database is updated, or when a webhook request hits the logic app itself as a webhook listener.

In this tutorial, I'll show you how to create and use logic apps in your own Shopify application. We're going to build an Azure logic app that will check the status of each user's Shopify subscription every 24 hours, and then send an email to the user if their subscription has renewed for another month.

If the job fails, we'll post a message to your Slack channel so you can fix it.

Checking Shopify subscriptions

Recently I had decided to switch over my app's billing system from Stripe to Shopify. I'm a huge fan of Stripe, and I use it wherever possible, but Shopify's billing offers something that Stripe doesn't: my customers don't have to give me their credit card to pay for the app. Needless to say, not having to ask for a credit card is invaluable -- it removes one of the biggest (probably the biggest) roadblocks to getting a customer to sign up.

By the way, I wrote a free guide to using Shopify's billing API. It's got all of the code you'll need to charge store owners for using your Shopify app. Even better, it includes an entire ASP.NET project that you can use as the foundation for a new app or drop into an existing one. Get the guide, for free, right here.

Unfortunately, one of the biggest tradeoffs I had to make was losing Stripe's various billing webhooks. They've got a bunch of them, and they'll tell your app about events such as a customer's charge being declined due to low funds, when a customer files a chargeback against you, when the customer's free trial is ending, etc.

The one that I miss the most, though, is the Subscription Renewed webhook. It tells your app that a customer's subscription has been renewed for another month. I used that webhook to compile a report to the customer and then email it to them with a note that thanked them for purchasing another month of service.

Billing webhooks like these don't exist with Shopify, though. If you want to recover any of Stripe's webhook functionality, you're expected to set up an automated job and write some code to do it yourself. Thankfully, Azure's logic apps finally make that possible.

The only thing that you'll need to run an Azure logic app is a valid Microsoft Azure account with a valid credit card linked to it.

Like many things in life, Azure isn't free. In fact, it's a bit more expensive than most web hosting, but I think the "quality of life" that you get from using Azure over Joe Random's ASP.NET Hosting company is easily worth the price hike. Happily, Azure does offer a free trial. If you haven't signed up yet, you can take advantage of that right here: https://azure.microsoft.com/en-us/pricing/free-trial.

Even better, though, is Microsoft's BizSpark program. If you're building your own software business or app, you can apply to BizSpark which will give you $150 of Azure credits, per month, for three years. I'm in the BizSpark program myself, and can highly recommend it. I've still got over a year of hosting left before I need to start paying for Azure, and you get all sorts of extra benefits like free copies of Microsoft Office and Visual Studio via MSDN.

Not all those who apply to BizSpark get in though, and it can take several weeks to be approved. You'll need your own website that describes the business you're building, too. If you've got what it takes, you can apply to the BizSpark program here: http://www.microsoft.com/bizspark.

Creating the Azure logic app

Once you've got your Azure account set up and ready to go, you can create your first logic app. This one is going to use a recurring trigger that makes a request to a certain URL on my app every 12 hours. Whenever my app receives that request, it will go through all users with active Shopify subscriptions and determine which ones had their subscription renewed since the last check.

On the left-hand menu, click "New => Web + Mobile => Logic App", give the new logic app a name and then create it. Don't worry about the "triggers and actions" setting just yet, we'll get to that after the app has been created.

Creating a new logic app

Once Azure has finished setting up your new logic app, open its "Triggers and actions" pane. Azure is going to show you a bunch of suggested recipes for your logic app, but you can disregard them and create one from scratch instead.

There's two parts to our "cron job" recipe: we need a recurrence trigger, and then an HTTP action. On the right side of the "Triggers and actions" pane, you'll have a list of, well, triggers and actions. Find the recurrence trigger and add it to the recipe. You'll need to decide how often you want this task to fire and then click the green check mark to confirm it. Personally, I like to run my job once every 12 hours.

The recurrence trigger

Next, add an HTTP action to the recipe. I want this action to make a POST request to e.g. "https://my-app.com/webhooks/ShopifyBilling", and it needs to send along an "X-CRON-AUTH" header. We're going to use that header to ensure the request is authentic and only coming from a logic app. You don't want a malicious user figuring out your cron URL and then forcing it to run this task thousands of times per hour.

The HTTP action expects your headers to be in JSON format, so to add the "X-CRON-AUTH" header, you'll need to enter this as the header value:

{ "X-CRON-AUTH" : "Your-Secret-Authorization-Key" }

Typically, I use a random GUID string for my authorization keys. They're impossible to guess, and easy to create. You can use Visual Studio to create a GUID by going to "Tools => Create GUID". You'll need to use it in your app too, though, so make sure you hang on to it.

The HTTP action

Confirm the HTTP action once you've got it set up by clicking the green check mark, and then save the entire trigger. Once every 12 hours, your new logic app is going to make a POST request to the URL you gave it and attach the X-CRON-AUTH header. Now, we need to handle the job and actually check for renewed subscriptions.

Handling the logic app's requests

Before we get started on the controller and action itself, we need to lay a tiny bit of groundwork by adding a property to your user model. To compare the user's billing date and check if they've renewed since the last time the job ran, we need to store that billing date in the database. It'll have to be nullable, because we won't be storing the date until the user accepts their subscription charge after they've created an account.

If you're using the app that we built in Shopify Billing 101, find the ApplicationUser class at Models/IdentityModels.cs. Add the following property:

...

public DateTime? BillingOn { get; set; }

You'll also need to fill that date as soon as the user accepts your subscription charge and you activate it. If you don't fill it immediately after activating, the job handler is going to accidentally send an email to new users thanking them for another month of subscription. That'll be pretty confusing when they've only been signed up for less than 12 hours.

In Shopify Billing 101, we activated the user's charge at ShopifyController.ChargeResult. Because Shopify doesn't return the updated charge after activating it, you'll need to pull it in again to refresh the BillingOn property.

Add the following code right after activating their charge:

//Activate the charge
await service.ActivateAsync(charge_id);

//Get the charge again to refresh its BillingOn date.
charge = await service.GetAsync(charge_id);

//Save the charge to the user model
user.ShopifyChargeId = charge_id;
user.BillingOn = charge.BillingOn;

Moving on, I've set my HTTP action to make its request to /webhooks/ShopifyBilling, which is where the app will pull in all of my active customers and check their subscription status. It's a POST request, so we'll need to mark the action with the HttpPost attribute. One of the delightful things about Azure logic apps is that they can record both the input and the output of all triggers and actions. For the HTTP action, that means it will record the response of your server for debugging purposes.

With that in mind, I want my action to return a string that's either going to say the action was successful, or an error describing what went wrong.

using ShopifyBilling.Models;
using ShopifySharp;
using System;
using System.Data.Entity;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web;
using System.Web.Caching;
using System.Web.Mvc;

namespace MyShopifyApp
{
	public class WebhooksController : Controller
	{
		//Pull in your X-CRON-AUTH key from your web.config file
		private static string CronAuthKey { get; } = ConfigurationManager.AppSettings.Get("cron-auth-key");

		[HttpPost]
		public async Task<string> ShopifyBilling()
		{
			// TODO
		}
	}
}

First thing's first, you need to make sure the request is valid by checking the X-CRON-AUTH header. If it's missing or doesn't match the key that your logic app is using, the request will be invalid and should not continue.

If the request is valid, we're going to pull in all of your app's active Shopify users and start to iterate over them. If you're using the app we built in Shopify Billing 101, you'll be able to tell that a user has an active subscription if their ShopifyChargeId and ShopifyAccessToken values aren't null.

var header = Request.Headers.Get("X-CRON-AUTH");

if(header == null || header != CronAuthKey)
{
	Response.StatusCode = (int) HttpStatusCode.Unauthorized;

	return "Invalid authorization header.";
}

using(var context = new ApplicationDbContext())
{
	var activeUsers = await context.Users.Where(x => x.ShopifyChargeId != null && x.ShopifyAccessToken != null).ToListAsync();

	//Iterate over each user to check their subscriptions
	foreach(var user in activeUsers)
	{
		// TODO
	}
}

return "Job finished successfully.";

Hopefully, at some point you're going to have way too many users with an active subscription to be doing this in a single ASP.NET request. Instead, you'll need to set up something like an Azure queue (which your logic app can talk to!) to handle everything we're doing here.

There are a few finicky things that we really need to be on the lookout for right here, or else we could end up sending the wrong type of email to a customer.

First, we're using ShopifySharp to pull in and check the user's subscription, so you'll need to be on the lookout for a "404 Not Found" exception when getting a subscription charge. If that exception gets thrown, it means the user has uninstalled your Shopify app and cancelled their subscription. If you've implemented the AppUninstalled webhook from this tutorial, your app should have already deleted their Shopify information ensuring they won't get pulled into this foreach loop.

Second, if the charge's status isn't Activated, we shouldn't do anything. In Shopify Billing 101, we didn't store a charge's id until it had been activated, so that's not something we should have to worry about here. However, there are a couple good reasons you'd want to store a charge id before it's activated, and if you're doing that you'll need to check that the charge is activated and skip sending emails if not.

Third and finally, you need to make sure you're setting their BillingOn date immediately after activating their subscription charge. If you don't do this, you'll need to check that their BillingOn date is null, or else your user is going to get an email thanking them for another month of support when they've only been signed up for less than 12 hours.

foreach(var user in activeUsers)
{
	var service = new ShopifyRecurringChargeService(user.MyShopifyDomain, user.ShopifyAccessToken);
	ShopifyRecurringCharge sub;

	try
	{
		sub = await service.GetAsync(user.ShopifyChargeId.Value);
	}
	catch(ShopifyException e) when (e.Message.Equals("Not found", StringComparison.OrdinalIgnoreCase))
	{
		// The user has uninstalled your app and cancelled their subscription
		user.ShopifyChargeId = null;
		user.ShopifyAccessToken = null;
		user.MyShopifyDomain = null;
		user.BillingOn = null;

		// TODO: Send an email asking for feedback on why the user uninstalled your app.
		continue;
	}

	//If the customer's BillingOn property is null, it means you didn't set it after activating their charge.
	if(user.BillingOn == null)
	{
		user.BillingOn = sub.BillingOn;

		//The user has just started their account. We don't want to send them a "Thanks for paying!" email.
		continue;
	}

	//Check if the user's next billing date no longer matches the one stored in the database.
	if(sub.BillingOn != user.BillingOn)
	{
		//User's subscription has been renewed within the last 12 hours. Update their BillingOn date.


		// TODO: Use .NET's SMTP service, or a 3rd-party like Mandrill or SendGrid to send an email
		// to the user, thanking them for their continued support.
	}
}

//Save all of the changes made to each user
try
{
	await context.SaveChangesAsync();
}
catch(Exception e)
{
	// TODO: Log and handle this exception.
	// NOTE: Emails have already been sent to users, but changes weren't saved.

	Response.StatusCode = 500;

	return "Failed to save changes. Reason: " + e.Message;
}

Important: you'll need to take into account that we're saving changes outside if the foreach loop. If an exception is thrown, nothing gets saved. Users will get duplicate emails the next time your logic app runs. To prevent that, you may want to move this SaveChanges call into the foreach loop. It's extremely unlikely that SaveChanges would fail with what we're doing here, so be aware that it's better, performance-wise to use a single SaveChanges over potentially hundreds.

Testing your new Azure-flavored "cron" job.

It's pretty easy to test an Azure logic app, even one that's supposed to run on a recurring schedule. Rather than waiting for it to run every 12 hours, you can just hit the big "test now" button. Of course, you should be aware that if it's actually hitting your app it could start firing emails off immediately.

Run now!

Here's what that request looks like when it's sent. I've circled the X-CRON-AUTH header in red, which is what my WebhooksController.AuthResult looks for to determine if the request is authentic.

The Azure cron job request

One of the nice things about the HTTP action when used in a logic app is that Azure keeps a record of the result returned by the server. It not only records the response body (the strings we're returning), but also the response headers. Find the job run you're looking for in the logic app's "Operations" history, click on the HTTP action, and then open the "Outputs" link.

The operations history

All of the triggers and actions in a logic app are tied together by their "Inputs" and their "Outputs". That is, you feed the output of one action into the input of the next action. An HTTP action's output is headers and body of the response, and another action in a logic app could use those headers and body for their own purposes.

This is a real response from my app's WebhooksController.ShopifyBilling action, recorded as the Output of my HTTP action:

Successful response

If it had returned an error, you'd see that right here too.

How can you tell when a job fails?

I've said it a few times, and I'm going to say it again: Azure logic apps are really souped-up cron jobs. The results of each trigger and action can be fed as the input to the next action, daisy-chaining them together to get some pretty advanced, well, logic. Unfortunately, Azure doesn't offer a way to directly send an alert when a logic app operation fails.

Luckily, we can use that action chaining to send an email, Slack message, or even an SMS text when our HTTP action fails. Slack is going to be the easiest one to set up, but you'll need a Slack account if you don't have one already. You can sign up for free at https://slack.com.

Once you've got your account, head back to the triggers and actions panel and add the Slack connector from the list of actions on the right. You'll be asked to authorize the connection if you haven't done so before. Once authorized, click the only available action for the Slack connector which is "Post message".

I don't like spam in my Slack channels, so let's add a condition that makes sure the Slack action will only run if your ShopifyBilling action returns a string other than "Job finished successfully". Click on the action's 3-dot menu and select "Add a condition".

Azure logic apps have a slightly confusing JavaScript-esque language that you need to use to refer to the inputs and outputs of triggers and actions. You can study up on it if you want right here, but I'll make it easy and give you the answers.

For the Slack action's condition, add the following string:

@not(equals(toLower('Job finished successfully.'), toLower(body('http'))))

Note that the single quote marks are very important! Don't use double quotes here or your message won't post. For the message's text, add this:

Azure cron job failed! Response: @{body('http')}

Slack integration

Go ahead and save the triggers and actions. If you want to see it for yourself, you can change the 'Job finished successfully' string in the Slack action's connector to something else, and then run the logic app. Just be sure to change it back when you're done.

With everything up and running, you should now receive a Slack message whenever your WebhooksController.ShopifyBilling action doesn't return the "Job finished successfully." string.

Slack error alerts

Note: If your WebhooksController.ShopifyBilling directly throws an exception, you're going to get a HUGE message in your Slack channel containing all of the HTML and the full stack trace from that exception. You'll need to take that into consideration.


Learn how to build rock solid Shopify apps with C# and ASP.NET!

Did you enjoy this article? I wrote a premium course for C# and ASP.NET developers, and it's all about building rock-solid Shopify apps from day one.

Enter your email here and I'll send you a free sample from The Shopify Development Handbook. It'll help you get started with integrating your users' Shopify stores and charging them with the Shopify billing API.

We won't send you spam. Unsubscribe at any time.