shopify

Shopify Webhooks Explained

How Shopify webhooks work — subscribing to topics, verifying the HMAC, the order and customer events that matter, and mandatory GDPR webhooks — a practical developer reference.

Shopify webhooks notify your app the moment something happens in a store — an order is paid, a customer is created, a subscription renews. You subscribe to specific topics, and Shopify POSTs a JSON payload to your endpoint for each matching event. This guide covers the topics worth subscribing to, how to verify deliveries, and the mandatory webhooks every app must handle.

Subscribing to topics

A webhook subscription pairs a topic (like orders/paid) with an endpoint URL. You can create subscriptions three ways:

  • Shopify admin — Settings → Notifications → Webhooks, for a single store.
  • Admin APIPOST a webhookSubscription via GraphQL or REST, for apps managing many stores.
  • App config — declare [[webhooks.subscriptions]] in shopify.app.toml so they’re registered on install.

Each delivery arrives as a JSON body with headers telling you which store and topic it’s for: X-Shopify-Topic, X-Shopify-Shop-Domain, and X-Shopify-Hmac-Sha256.

Verifying the HMAC

Verify every delivery before trusting it. Shopify signs the raw request body with your app’s secret and puts the result in the X-Shopify-Hmac-Sha256 header. Recompute it and compare:

const crypto = require('crypto');

function verify(rawBody, hmacHeader, secret) {
  const digest = crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('base64');
  return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(hmacHeader));
}

As with Stripe, compute the HMAC over the raw request body, not a re-serialized version of the parsed JSON — reformatting changes the bytes and the signature won’t match. Use a constant-time comparison (timingSafeEqual) rather than ===.

The events that matter

TopicWhat it means
orders/createA new order was placed (may not be paid yet)
orders/paidPayment captured — the revenue signal for most stores
orders/fulfilledThe order shipped
orders/cancelledAn order was cancelled
refunds/createA refund was issued
customers/createA new customer record
checkouts/createA checkout started — useful for abandoned-checkout flows

For most stores, orders/paid is the one to alert on: it fires when money is actually captured, whereas orders/create can fire for unpaid or draft orders.

Retries and duplicates

Shopify expects a 2xx response quickly. If your endpoint fails or is too slow, Shopify retries over ~48 hours, then removes the subscription if it keeps failing. As with any webhook source, deliveries can repeat, so dedupe on the order or event ID and keep your handler idempotent. Return 2xx first, then do the slow work.

Don’t forget the mandatory GDPR webhooks

Public apps in the Shopify App Store must implement three compliance topics or they’ll fail review:

  • customers/data_request — a customer requested their data.
  • customers/redact — delete a customer’s data.
  • shop/redact — delete a shop’s data (sent 48 hours after an app is uninstalled).

These are easy to forget because they’re not about your core feature — but they’re required.

Turn orders into alerts

If the goal is simply to see paid orders (and who placed them) in a team channel, you don’t need to build and host any of this. Notipus verifies the HMAC, enriches each order with customer context, and posts it to Slack, Telegram, or Microsoft Teams. See the Shopify integration for the full setup.

Get Shopify orders in Slack — no endpoint to build

Notipus verifies your Shopify webhooks and posts each order to Slack, Telegram, or Microsoft Teams with customer context. Connect your store and pick a channel.

Start free