Everything you need to know about Shopify's AppUninstalled webhook.

(This article is a follow-up to the Shopify application we built in Shopify Billing 101. You can get a free copy of that guide by clicking right here and entering your email address.)

Webhooks can be a little bit confusing, especially if you've never used them before. The benefit that you get from using them, though, is immense — especially if you're building or running a Shopify application for the Shopify app store. A webhook is a kind of contract between you and a service, and they're built for handling specific events, such as when a new order has been placed.

Rather than setting up cron jobs (which is nearly impossible in ASP.NET) and polling Shopify's API for new orders every 10 or 15 minutes, you can set up a webhook and have Shopify tell you about a new order — immediately after it's been placed. To do that, Shopify (or whatever service is providing the webhook) will ping a certain URL on your site with data about the new event.

In ASP.NET terms, you can think of a webhook as a kind of request hitting a controller's action, except that this controller and action are dedicated to webhooks and they should not be hit by actual users.

There are two kinds of webhooks in the world:

  • The firehose webhook: You give the service a single URL, and then they'll hit that URL with every single event that occurs on your Stripe account. The request's JSON payload will contain an "event type" property that you can use to determine what should happen with that quest — process it or just return an HTTP OK status. Stripe is a great example of the firehose webhook.
  • The fine-grained webhook: You set up multiple webhook URLs, and each URL is only used for a specific event "topic". Instead of reading the JSON payload to determine the event type, you know that any request to a URL only deals with one type of event.

Shopify's webhooks are of the fine-grained flavor. You create a webhook for every shop that connects to your app, and each webhook has a unique URL and topic. There are a bunch of different topics and events that you can create a webhook for, including when an order is created, when a shop has updated their name, when a customer has started the checkout process, and so on.

No matter what the topic, you'll have to write code that will make your app specifically subscribe to a webhook. Shopify won't just send them to you willy-nilly — after all, it would be a huge waste of bandwidth if Shopify sent you webhooks for every event that occurs if you're only going to use one or two of them.

The AppUninstalled webhook

In my view, the AppUninstalled webhook is easily the most important one that you can use, and should always be used no matter what kind of app you're building. The AppUninstalled webhook will be triggered whenever a shop that has already installed your app uninstalls your app. Here's what you really need to know about that event:

  1. When the user uninstalls your app and this webhook is subsequently fired, your access token to the user's shop will have already been invalidated. If you try to use it, Shopify will return an error that says the access token is invalid. If you're using ShopifySharp for interacting with the Shopify API in your project, this will throw an exception.
  2. When the user uninstalls your app and this webhook is subsequently fired, any recurring charge that the user had to your app will have already been deleted.

If you're using the application that we built in Shopify Billing 101 (the free guide to the Shopify billing API), then there are a couple of things that you'll need to do to set up and properly handle the AppUninstalled webhook. In this scenario, we'll be upgrading that app to create an AppUninstalled webhook as soon as the user connects their Shopify store to your app. Then, we'll create the webhook controller and action.

First thing's first, let's use ShopifySharp's ShopifyWebhookService to quickly create the webhook. If you don't have ShopifySharp installed in your project, just type this command into your Package Manager Console in Visual Studio.

install-package ShopifySharp

In the application that we built in the free guide to the Shopify billing API, we set up a ShopifyController that had an action named AuthResult. That action is where we finalized your app's installation to the user's Shopify store, and then we generated an access token that was stored in your app's database. Now, it's important to create this particular webhook as soon as you possibly can -- the user might step away from their keyboard after connecting their store, and then come back later to uninstall your app after deciding that they don't really need it right now.

With that in mind, we're going to create this webhook immediately after getting their access token. Open up your ShopifyController under the "Controllers" folder in your project, and then find the AuthResult action. (If you've modified the app that we built, or you're using your own custom app, just go to the method that generates an access token to the user's store). Add the following to create the AppUninstalled webhook.

One hugely important thing to notice here is that the webhook's Address cannot be a localhost URL. Instead, you'll need to publish your app online at your own domain, and then update the app's URL and Redirect URLs in your Shopify partner dashboard. Now if you don't have your own domain yet, there's actually one way that you can create a webhook here while you're testing your app.

Unlike Redirection URLs, a webhook's URL doesn't have to be the same domain as the app's default URL. That means that if the app URL you set in your Shopify partner dashboard is e.g. "http://example.com/shopify/handshake", your webhooks are not constrained to the same "example.com" domain.

Instead, you could set up your webhook to point to a service like RequestBin, where you can manually inspect the entire webhook request once it's fired. You should be aware, though, that RequestBins only last for 24 hours, and then they're deleted and will return an error to the webhook service. That means you're going to get a ton of errors in your email inbox if you create a webhook and point it to RequestBin, and then Shopify tries to send a webhook to the RequestBin after it has expired.

More importantly, though, you won't be able to handle the webhook if it isn't pointing to your own app! I strongly, strongly recommend that you only use RequestBin here if you don't have a domain and can't get one any time soon.

Before we continue on toward handling the webhook, there are three more things that you should know about creating a webhook.

First, each webhook's URL must be unique. That means you can only create one webhook with its address pointing to e.g. "https://example.com/webhook/handle". If you try to create any more webhooks with that same address, the Shopify API will return an error and ShopifySharp itself will throw an exception.

(It should be noted, though, that a querystring can make a webhook URL unique, thereby allowing multiple webhooks with the same domain and path.)

Second, every webhook has a format property that you can set to either "json" or "xml". This property will control the format that Shopify will deliver your webhook in. Personally, I would recommend JSON as it's easier to parse and easier to read when compared to XML. The rest of this article will assume that you've set your format to "json".

Finally, I've appended a userId parameter to our AppUninstalled webhook's address. That parameter will tell us exactly which user we should pull in when handling the webhook. While Shopify actually sends an "X-Shopify-Shop-Domain" header along with each webhook request, I like to use the userId parameter so that I can pull in the user's model by its id, rather than its shop URL. It's just a matter of personal preference, though.

Handling the AppUninstalled webhook

That's all that it takes to create a webhook. Now we'll need to create the controller and action that will handle it. If you take a look at the Address property of the webhook, you'll see that we need to create a new controller named WebhooksController, and we'll give it a public, async action named AppUninstalled. Unlike most actions in your typical controller, this one will return a string rather than a view or a redirect — and that string is only for our benefit, as Shopify just expects your controller to return a 200 OK response.

If you recall when we built the ShopifyController.AuthResult action, the very first thing we did was to validate that the request coming from Shopify was authentic. We're going to use ShopifySharp to do the exact same thing here. After all, you don't want a malicious user to figure out your webhook URL and then force your app to uninstall a user's Shopify app.

Behind the scenes, ShopifySharp is taking a header named "X-Shopify-Hmac-SHA256", which contains a hash of your secret key and the request's entire body, and then creates its own hash from the same properties. If the two hashes match, then the request is authentic and valid. Every Shopify webhook contains that header hash. It can only be spoofed by somebody that knows your app's secret key — which should only be you and Shopify.

Remember, the webhook's URL also contained a userId parameter. The new action will receive that parameter as a string and use it to instantly pull in the user model from your app's database. When this webhook is fired, we know the following two things.

  1. Your access token to the user's shop is no longer valid.
  2. If the user was subscribed to your app via the Shopify billing API, their subscription has been deleted.

Because the access token will have been invalidated by Shopify at this point, any attempts to call the Shopify API with it will fail, and ShopifySharp itself will start throwing exceptions. With that in mind, you should delete the user's access token and Shopify domain from your database, along with their subscription charge id. If they ever try to come back and log in to your app, they'll need to reconnect their store and accept a new Shopify subscription charge.

That's all it takes to handle the AppUninstalled webhook. Again, no matter what, if you receive this webhook (and it's valid), the user has already uninstalled your app, any recurring/subscription charges to your app have been canceled, and you can no longer use their shop's access token.

In my own Shopify app, I take a moment in this webhook to send an email to the user that will tell them what happened, and gently remind them wha they need to do to start using the app again.

But sending emails is beyond the scope of this article, so let's continue on and test to ensure this webhook is still working. To do that, I'm going to install my app to a test development store, and then uninstall it. At that point, Shopify should immediately send the webhook to my domain where the app will delete the access token and charge.

(Again, this article assumes that you've published your app online at its own domain. Shopify will not send webhooks to a localhost URL.)

Once you've got the app installed to your development store, find it in the list of installed apps on the store's admin dashboard. Click the 3-dot menu and uninstall it.

Uninstalling the app

Then, head over to your app and try to navigate to the /Dashboard page that we protected with [RequireSubscription]. (If you haven't read the guide where we created that attribute, all you need to know is that the /Dashboard page checks the user model in the database to ensure that ShopifyAccessToken and ShopifyChargeId are not null.)

Connect your shop

If the webhook worked, you should be redirected to connect your Shopify store.


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.