January 28, 2024

9 minutes read

Shopify webhooks: what they are and how to use them

How to create and listen for webhooks from Shopify.

Rafael Danilo


1. What are webhooks?

Webhooks are a powerful tool for integrating third-party services with your application. They offer a way for these services to notify your application in real-time when something important occurs. In the world of Shopify Apps, webhooks allow you to get notified about events within a store that installed your app. For example, Shopify can let you know when an order gets placed in a store, or when a customer's profile gets updated.

There are actually hundreds of events or "topics" you can listen for via Shopify webhooks. Check out this page for a comprehensive list.

Webhooks versus normal API calls

You might wonder why use webhooks instead of hitting the Shopify API directly. After all, any information you can read via a Shopify webhook you could also read from API endpoints. For example, if you're interested in tracking the latest orders placed by one of the stores that installed your app, you could use the /orders.json endpoint of their REST Admin API, like so:

curl "https://STORE.myshopify.com/admin/api/2023-01/orders.json?status=any&created_at_min=TIMESTAMP" \
  -H "X-Shopify-Access-Token: ACCESS_TOKEN" \
  -H "Content-Type: application/json"

This should return you all the orders of a store which were placed since TIMESTAMP.

Alternatively, you could create a webhook for the topic orders/create and ask Shopify to let you know, by making a request to your server, whenever a new order is placed.

Compared to the first approach, using a webhook offer several advantages. First, they are real-time, meaning you don't have to wait until you hit /orders.json for a particular store to hear about new orders. That also makes them easier to reason about, because you don't have to worry about frequently hitting the API, for each client store, nor keeping track of where you left of etc. Finally, webhooks also make your server more resource-efficient because Shopify is reaching out to you instead of the other way around.

Webhooks have their own downsides, however, especially when it comes to data integrity. We will talk more about this below.

Who this guide is for

This guide is aimed at app developers looking to build better integrations with Shopify stores. Webhooks can also be relevant for store owners, who should check out this Shopify guide here. Throughout this guide, we'll use Typescript and HTTP in our code examples. Even if you're not familiar with these technologies, the concepts and logic should be easily transferrable to other programming languages.

2. Creating Shopify webhooks

To register Shopify webhooks, we'll need to use the Shopify Admin API. In Shopify, webhooks are isolated at a store level. That means you'll need to register individual webhooks for each store you weant to receive notifications from. Let's dive right into a practical example.

Example: Slack notifications for Shopify

Suppose you're building a Shopify app that notifies store owners via Slack whenever an order gets created or updated on their store. The app store is filled with apps that do this. As we've seen, you have two options. You can frequently query the /orders.json endpoint of the Shopify Admin API to check for new orders. Or, the superior alternative, you can use webhooks to ask Shopify to let you know whenever a new order gets placed in a store. We'll go with webhooks.

To manage webhooks via the REST Admin API, we'll need two things to start:

  • The "myShopify" domain of a particular store. This is the unique identifier of a store within the Shopify ecossystem, always in the format STORE.myshopify.com. You can read more about this here.
  • The "access token" for that store, which is required to authenticate the call. You obtain this when a store installs your app via OAuth.

Need help using OAuth to obtain the access token for a store? Check out our open-source project Handshake, a self-hosted solution for handling OAuth for Shopify and dozens of other APIs. It's built for easy self-hosting on Vercel.

With these credentials at hand, here's our code for registering webhooks.

import assert from 'assert';
// FIXME replace. We'll talk about what to put here in the next section.
const WEBHOOK_ENDPOINT = 'https://SOMETHING/listeners';
const API_VERSION = '2024-01';
	// ...whatever else...
 * Makes sure all the necessary webhooks are registered for this particular
 * store.
 * Call this every time a user goes through the OAuth flow.
async function registerWebhooksOnAuth(
	shopifyDomain: string,
	accessToken: string
) {
	for (const topic of PRODUCT_TOPICS) {
		await registerWebhookTopic(
			// This is where Shopify will attempt to notify you when an event
			// occurs on this topic.
 * Tells Shopify to let you know about events in topic `topic` for
 * this store at `webhookEndpoint`.
async function registerWebhookTopic(
	shopifyDomain: string,
	accessToken: string,
	topic: string,
	webhookEndpoint: string
): Promise<Webhook> {
	const url = `https://${shopifyDomain}/admin/api/${API_VERSION}/webhooks.json`;
	const res = await fetch(url, {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
			'X-Shopify-Access-Token': accessToken,
		body: JSON.stringify({
			webhook: {
				address: webhookEndpoint,
				format: 'json',
	if (!res.ok) {
		const body = await res.text();
		throw Error(
			`Failed to register webhook at url=${url} status=${res.status} body=${body}`
	const json = await res.json();
	console.log(`Webhook for topic ${topic} registered successfull.`, json);
	return json.webhook;
async function main() {
	const shopifyDomain = process.env.SHOPIFY_DOMAIN ?? '';
	assert(shopifyDomain.length > 0, 'specify a SHOPIFY_DOMAIN env variable');
	const accessToken = process.env.ACCESS_TOKEN ?? '';
	assert(accessToken.length > 0, 'specify a ACCESS_TOKEN env variable');
	// Image this user just completed OAuth and you (re-)acquired their access token
	// after the handshake with Shopify.
	await registerWebhooksOnAuth(shopifyDomain, accessToken);
void main()

Let's break it down. The registerWebhookTopic takes in the credentials of a particular store, together with a webhook "topic" and an endpoint to receive Shopify requests. It then makes a POST request to https://${shopifyDomain}/admin/api/2024-01/webhooks.json to create the webhook object and begin to receive updates.

This code subscribes to four topics (ie. events) we are interested in: orders/create, orders/paid, orders/fulfilled, and orders/updated. To read about all the available topics and what causes Shopify to trigger them, check out their API documentation.

It's important to note that webhooks in Shopify have scope requirements. For example, orders/create requires the access token to allow the read_orders scope, as well as access to the new "Protected Customer Data" permission.

3. Receiving webhooks locally

So far we've covered how to register webhooks but not what it looks like when a webhook gets fired to your endpoint. And since every software development begins locally, we'll start by talking about how to receive webhooks to your computer.

Writing a webhook listener

Let's create a simple server using Fastify to receive webhook requests and print out their content.

import fastify from 'fastify';
const app = fastify();
app.post<{ Body: { username: string } }>(
	async (req, reply) => {
		console.log('Webhook request received')
		console.log('body is', req.body);
		console.log('headers are', req.headers);
		// ...
		// await forwardOrderEventToClientSlack();
		reply.code(200).send({ message: 'OK' });
app.listen({ port: 4444 }).then(() => {
	console.log('Server running :)');

Shopify requires you to respond to webhook requests with HTTP status 200, to indicate that the message was succesfully acknowledged. You must also do so within two seconds. Failure to respond with 200 in two seconds will cause Shopify to consider this webhook delivery as failed.

Exposing your listener to the world

One major challenge when working locally is that Shopify cannot directly deliver data to your localhost. This is for a variety of reasons. Your computer is behind a myriad of barriers like ISPs, routers, and firewalls. These not only make it hard to reach your machine but also to uniquely identify it (as "localhost:4000" is anything but unique).

Using Ngrok

To solve this, we'll need to use a tool like Ngrok. Ngrok allows you to route a public URL, such as fiber-dev.ngrok.io, to a server running in your localhost. It does so via a mechanism called "reverse proxying", which is beyond the scope of this article.

To install ngrok on Mac OS using Homebrew:

brew install ngrok/ngrok/ngrok

Head to ngrok.com/download to read instructions for installing Ngrok in other setups.

With Ngrok installed, you can expose a server running on port (eg. 4000) with the following command:

ngrok http 4000 -subdomain=fiber-dev

You should see output like the following:


With this running, you can go navigate to fiber-dev.ngrok.io or whatever your public URL is on your browser. If you don't have a server running on 4000, you will see the following error. This indicates Ngrok is running as expected.


Now you're ready to go back to the registration code of the previous section and replace the WEBHOOK_ENDPOINT definition with your Ngrok URL eg. https://fiber-dev.ngrok.io.

import assert from 'assert';
// FIXME replace. We'll talk about what to put here in the next section.
const WEBHOOK_ENDPOINT = 'https://fiber-dev.ngrok.io/listeners';
const API_VERSION = '2024-01';

Now we can run your server (here we're using Bun):

$ bun run registrar.ts
Webhook for topic orders/create registered successfull.
Webhook for topic orders/paid registered successfull.
Webhook for topic orders/fulfilled registered successfull.
Webhook for topic orders/updated registered successfull.

Sending a test request

Now, with the server running, we are ready to test the webhook handler we created. In a separate terminal session, fire a request to create a new order object:

curl -X POST https://STORE.myshopify.com/admin/api/2024-01/orders.json \
	-H "X-Shopify-Access-Token: ACCESS_TOKEN" \
	-H "Content-Type: application/json" \
	-d '{ "order": { "line_items": [{ "name": "apple", "title": "Apple", "price": "12.3" }] } }'

You should see a log like this pop up on your server:

Webhook request received.
body is {
  admin_graphql_api_id: "gid://shopify/Product/7864080892057",
  body_html: null,
  created_at: "2024-02-16T18:16:02-05:00",
  handle: "all-you-need-is-a-title-5",
  id: 7864080892057,
  product_type: "",
  published_at: "2024-02-16T18:16:02-05:00",
  template_suffix: null,
  title: "All you need is a title",
  updated_at: "2024-02-16T18:16:03-05:00",
  vendor: "Quickstart (e7b71ff6)",
  status: "active",
  published_scope: "global",
  tags: "",
  variants: [...],
  options: [...],
  images: [],
  image: null,
  variant_ids: [
      id: 43847111409817
headers are {
  host: "shopify-example.ngrok.app",
  "user-agent": "Shopify-Captain-Hook",
  "content-length": "1246",
  accept: "*/*",
  "accept-encoding": "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
  "content-type": "application/json",
  "x-forwarded-for": "",
  "x-forwarded-host": "shopify-example.ngrok.app",
  "x-forwarded-proto": "https",
  "x-shopify-api-version": "2024-01",
  "x-shopify-hmac-sha256": "w/...../SLF5sx13lFiNdcOmEE=",
  "x-shopify-product-id": "7864080892057",
  "x-shopify-shop-domain": "STORE.myshopify.com",
  "x-shopify-topic": "orders/create",
  "x-shopify-triggered-at": "2024-02-16T23:16:02.916424605Z",
  "x-shopify-webhook-id": "93731d26-765c-41b1-b079-...."

That's it! At this point, your server has been able to receive and acknowledge an incoming webhook. This same mechanism can be used to receive all the events we've specified.

In the next post of this series on webhooks, we will cover verifying the source of webhooks and how to safeguard your servers against malicious requests.