Webhooks
Polling the Admin API on a timer to check "did anything change?" is wasteful and slow to react. Webhooks flip that around: you tell Shopify which events you care about, and Shopify sends your server an HTTP request the moment one happens.
Registering a webhook
A webhook subscription is created through the Admin API (or, for simple cases, the Shopify Admin UI) by specifying a topic and a destination URL:
mutation {
webhookSubscriptionCreate(
topic: ORDERS_CREATE
webhookSubscription: {
callbackUrl: "https://myapp.example.com/webhooks/orders-create"
format: JSON
}
) {
webhookSubscription { id }
}
}
From then on, every time an order is created on that store, Shopify sends a POST request with that order's data to your callback URL — no polling involved.
Common event topics
Shopify defines topics for nearly every meaningful store event — orders/create, orders/paid, orders/fulfilled, products/update, customers/create, app/uninstalled, and many more. Subscribing to only what you actually need matters: each active subscription is a live obligation your server has to keep responding to reliably, so an app typically requests the smallest set of topics its feature actually requires.
Handling the request
app.post('/webhooks/orders-create', (req, res) => {
const order = req.body;
console.log(`New order: ${order.id}`);
res.sendStatus(200);
});
Responding quickly matters — Shopify expects a fast 200 acknowledgment and will retry a webhook that times out or errors, so any slow processing (sending emails, updating a database) should happen after responding, not before.
Verifying authenticity with HMAC
Because the callback URL is publicly reachable, anything could POST to it claiming to be Shopify. Every real webhook request carries an X-Shopify-Hmac-SHA256 header — a signature computed from the request body and your app's shared secret — that you must recompute and compare before trusting the payload:
const digest = crypto
.createHmac('sha256', appSecret)
.update(rawBody, 'utf8')
.digest('base64');
const isValid = crypto.timingSafeEqual(
Buffer.from(digest),
Buffer.from(req.get('X-Shopify-Hmac-SHA256'))
);
Handling retries and duplicates
Because Shopify retries a webhook that doesn't acknowledge successfully, a handler can occasionally receive the same event more than once — a dropped connection on Shopify's side doesn't necessarily mean your handler didn't actually process it. Well-built webhook handlers are idempotent: processing the same order-creation event twice should produce the same end state as processing it once, typically by checking whether that event's ID has already been handled before doing any real work.
timingSafeEqual instead of a plain === comparison matters here — a naive string comparison can leak timing information an attacker could use to guess the correct signature byte by byte.