crumpled paper texture
Back to Shopify Articles
ShopifyFebruary 18, 2025(Updated: Feb 20, 2025)12 min read

Shopify Webhooks: A Complete Guide for Developers

A comprehensive, production-tested guide to Shopify webhooks: HMAC signature verification, raw body handling, idempotency with Redis, queue architecture, and local testing.

Yash Nandvana

Yash Nandvana

Full Stack Developer

Shopify Webhooks Architecture Diagram

1. Introduction

In modern e-commerce engineering, state changes occur constantly. Orders are placed, customer profiles are updated, inventory counts fluctuate across fulfillment centers, and merchants uninstall applications.

For developers building Shopify apps or custom integrations, discovering these state changes efficiently is a fundamental architectural requirement.

If your application polls Shopify's Admin API every 30 seconds asking "Has anything changed?", you encounter immediate scaling bottlenecks:

API Rate Limit Exhaustion: Polling quickly consumes your REST or GraphQL bucket allowances.
High Latency: Events occurring seconds after a poll must wait until the next cycle to be processed.
Wasted Compute & Cost: Over 99% of polling requests return zero changes, wasting server infrastructure and network bandwidth.

To solve this, Shopify provides Webhooks—an event-driven notification mechanism where Shopify pushes data to your HTTP server instantly whenever a relevant event occurs.

2. What Are Shopify Webhooks?

A Shopify webhook is an automated HTTP POST request sent from Shopify's servers to a designated HTTPS URL (your webhook endpoint) when a specific event takes place within a Shopify store.

Webhooks operate on a Publisher/Subscriber model:

1
Subscriber (Your App): Registers interest in specific events (e.g. orders/create) and provides a secure HTTPS callback URL.
2
Publisher (Shopify): Executes an HTTP POST payload containing detailed JSON representation of the event to your endpoint.

Real-World Use Cases

Inventory Synchronization: Update external ERP or warehouse inventory instantly when an inventory_levels/update webhook fires.
Order Fulfillment Automation: Trigger automated shipping labels or third-party logistics (3PL) workflows on orders/paid.
GDPR & Compliance: Process mandatory data erasure requests via customers/redact and shop/redact.
App Lifecycle Management: Clean up stored access tokens and merchant database records on app/uninstalled.

3. High-Level Event-Driven Architecture

In production, handling webhooks synchronously inside your web server route is a major architectural anti-pattern. Shopify expects your server to respond with an HTTP 200 OK status within 5 seconds. If your endpoint takes longer or crashes, Shopify considers the delivery failed and will attempt retries with exponential backoff.

Recommended Asynchronous Architecture

text
┌─────────────────┐       HTTP POST Payload        ┌────────────────────────┐
│  Shopify Store  │ ─────────────────────────────> │  API Gateway / Router  │
└─────────────────┘  (HMAC Signature Header)       └────────────────────────┘
                                                               │
                                                               │ 1. Fast Verification
                                                               │ 2. Enqueue Job
                                                               ▼
┌─────────────────┐       Async Job Process        ┌────────────────────────┐
│ Worker Process  │ <───────────────────────────── │  Redis / BullMQ Queue  │
│  (DB Updates)   │                                └────────────────────────┘
└─────────────────┘
1
API Router: Receives the incoming request, validates the HMAC-SHA256 signature, pushes the payload into a durable message queue (e.g. BullMQ / Redis), and immediately returns an HTTP 200 OK.
2
Worker Process: Consumes jobs from the queue asynchronously, executing heavy business logic, external API calls, and database updates.

4. Webhook Registration Methods

Shopify allows registering webhooks through three distinct mechanisms:

Method 1 — GraphQL Admin API (Recommended)

Registering webhooks programmatically via the GraphQL Admin API allows dynamic endpoint configuration upon app installation.

graphql
mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {
  webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
    userErrors {
      field
      message
    }
    webhookSubscription {
      id
      topic
      endpoint {
        ... on WebhookHttpEndpoint {
          callbackUrl
        }
      }
    }
  }
}

Variables Payload:

json
{
  "topic": "ORDERS_CREATE",
  "webhookSubscription": {
    "callbackUrl": "https://api.myapp.com/webhooks/orders-create",
    "format": "JSON"
  }
}

Method 2 — Shopify CLI App Configuration (shopify.app.toml)

For modern Shopify apps built with the Remix or Node app templates, webhooks can be declared declaratively in shopify.app.toml:

toml
[[webhooks.subscriptions]]
topics = [ "orders/create", "orders/edited" ]
uri = "/api/webhooks"

5. Security & HMAC Verification

Because webhook endpoints are public HTTPS URLs, never trust incoming requests without verifying their origin. Malicious actors could send forged payloads to corrupt your database.

Shopify signs every webhook payload with a cryptographic HMAC-SHA256 hash generated using your app's Client Secret.

HTTP Headers Included by Shopify

X-Shopify-Hmac-Sha256: Base64-encoded HMAC-SHA256 signature string.
X-Shopify-Topic: The event topic (e.g. orders/create).
X-Shopify-Shop-Domain: The merchant's myshopify.com domain handle.
X-Shopify-Webhook-Id: Unique UUID identifying the webhook event.

Verifying HMAC Signature in Node.js / Express

CRITICAL REQUIREMENT: HMAC calculation MUST use the raw unparsed request body string or buffer. If body-parser middleware has already transformed the request into a JSON object, stringifying it again will reorder keys and cause verification to fail!
javascript
import crypto from 'crypto';

export function verifyShopifyHmac(req, res, next) {
  const hmacHeader = req.get('X-Shopify-Hmac-Sha256');
  const clientSecret = process.env.SHOPIFY_API_SECRET;

  if (!hmacHeader) {
    return res.status(401).send('Missing HMAC header');
  }

  // req.rawBody must be populated by express.raw() or a custom verify function
  const calculatedHmac = crypto
    .createHmac('sha256', clientSecret)
    .update(req.rawBody, 'utf8')
    .digest('base64');

  const hmacValid = crypto.timingSafeEqual(
    Buffer.from(calculatedHmac),
    Buffer.from(hmacHeader)
  );

  if (!hmacValid) {
    console.error('HMAC Verification Failed!');
    return res.status(401).send('Unauthorized webhook source');
  }

  return next();
}

6. Preserving Raw Body in Express Middleware

To ensure req.rawBody is preserved while still supporting JSON bodies elsewhere:

javascript
import express from 'express';

const app = express();

// Capture raw body specifically for webhook routes
app.use(
  express.json({
    verify: (req, res, buf) => {
      req.rawBody = buf.toString('utf8');
    },
  })
);

7. Handling Idempotency & Retries

Network fluctuations or server restarts can cause Shopify to deliver the same webhook multiple times. If your application processes an orders/paid event twice without idempotency protection, you risk double-shipping or duplicate billing.

Idempotency Strategy using Redis

Shopify sends a unique X-Shopify-Webhook-Id with every payload. Before processing a payload, check whether the ID has already been recorded in Redis:

javascript
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);

export async function processWebhook(webhookId, payload) {
  const redisKey = `webhook:processed:${webhookId}`;
  
  // Set key only if it doesn't exist (NX), with a 24-hour expiration (86400s)
  const isNew = await redis.set(redisKey, '1', 'EX', 86400, 'NX');

  if (!isNew) {
    console.log(`Duplicate webhook ${webhookId} ignored.`);
    return { status: 'duplicate' };
  }

  // Execute actual business logic here
  await handleOrderCreated(payload);
  return { status: 'processed' };
}

8. Local Testing & Webhook Debugging

During local development, your local Node server (localhost:3000) cannot receive HTTP calls directly from Shopify's public servers.

Recommended Local Workflow

1
Shopify CLI Tunneling: Run shopify app dev which automatically provisions a secure Cloudflare tunnel to your dev server.
2
CLI Event Triggering: Test your handlers without creating real store orders:
bash
    shopify app webhook trigger --topic=ORDERS_CREATE --address=https://your-tunnel-url/api/webhooks
3
Webhook Log Inspection: View real-time webhook status and delivery logs in your Shopify Partner Dashboard under Apps > App Details > Webhooks.

9. Webhook Security & Production Checklist

[ ] HMAC Signature verification active on all webhook routes.
[ ] Raw body preserved before JSON middleware parsing.
[ ] Response returned with 200 OK in under 5 seconds.
[ ] Heavy business logic offloaded to asynchronous queues (BullMQ / Redis).
[ ] Idempotency checks configured using X-Shopify-Webhook-Id.
[ ] Mandatory GDPR compliance topics configured (customers/redact, shop/redact).
[ ] Constant-time comparison (crypto.timingSafeEqual) used for HMAC strings to prevent timing attacks.

10. Conclusion

Mastering Shopify webhooks is essential for building robust, event-driven Shopify applications. By combining HMAC verification, raw body preservation, queue-based async processing, and idempotency tracking, you ensure your app scales safely in production.

Next Recommended Reads:

[Shopify GraphQL Admin API: A Practical Guide for Developers](/blog/shopify/shopify-graphql-admin-api-guide)
[Building a Production-Ready REST API with Node.js, PostgreSQL & Prisma](/blog/full-stack/nodejs-postgresql-prisma-rest-api)
#Shopify#Webhooks#Node.js#Express#GraphQL#Redis#Security#Backend
Yash Nandvana

Yash NandvanaFull Stack Developer

Full Stack & Shopify Developer building scalable web apps, developer tools, and AI solutions.

Learn more about Yash
wingsLogo

FROM CONCEPT TO CREATION

LET'S MAKE IT HAPPEN!

I'm available for full-time roles & freelance projects.

I thrive on crafting dynamic web applications, and
delivering seamless user experiences.