Back to Blog
·Hook Mesh Team

Handling Shopify Webhooks: A Complete Guide

Learn how to receive and process Shopify webhooks reliably. This guide covers webhook topics, registration methods, HMAC verification, common challenges, and best practices for e-commerce integrations.

Handling Shopify Webhooks: A Complete Guide

Handling Shopify Webhooks: A Complete Guide

Shopify webhooks let your application respond instantly to store events—orders, products, customers—instead of constantly polling the API. Handling them reliably requires understanding registration methods, signature verification, and retry behavior.

This guide covers everything e-commerce developers need: practical code examples and solutions to common production challenges.

Understanding Shopify Webhook Topics

Shopify organizes webhooks around topics—specific events that trigger notifications. Choose the right topics to receive only the data you need.

Order Webhooks

Order webhooks are the most commonly used for e-commerce integrations:

  • orders/create: Fires when a customer completes checkout. Use this to trigger fulfillment workflows, update external inventory systems, or send order data to your ERP.
  • orders/updated: Triggers when any order attribute changes, including status updates, address edits, or line item modifications.
  • orders/paid: Specifically fires when payment is captured—useful for apps that should only act on confirmed payments.
  • orders/cancelled: Notifies when orders are cancelled, enabling automatic refund processing or inventory restoration.
  • orders/fulfilled: Triggers when all items in an order are marked as fulfilled.

Product Webhooks

Product webhooks keep external systems synchronized with your catalog:

  • products/create: Fires when new products are added to the store.
  • products/update: Triggers on any product change—title, description, price, variants, images, or inventory levels.
  • products/delete: Notifies when products are removed from the catalog.

Customer Webhooks

Customer webhooks power personalization and CRM integrations:

  • customers/create: Fires when new customer accounts are created.
  • customers/update: Triggers on profile changes, including address updates and marketing preferences.
  • customers/delete: Notifies when customer data is removed (important for GDPR compliance).

Fulfillment Webhooks

Fulfillment webhooks enable shipping and logistics integrations:

  • fulfillments/create: Fires when fulfillment records are created for orders.
  • fulfillments/update: Triggers when tracking information is added or fulfillment status changes.

Registering Shopify Webhooks

Shopify provides two registration methods, each suited to different use cases.

Method 1: Shopify Admin API

Register webhooks programmatically using the Admin API for custom apps and integrations.

const Shopify = require('@shopify/shopify-api');

async function registerWebhook(session) {
  const response = await Shopify.Webhooks.Registry.register({
    shop: session.shop,
    accessToken: session.accessToken,
    path: '/webhooks/orders',
    topic: 'ORDERS_CREATE',
    webhookHandler: async (topic, shop, body) => {
      const order = JSON.parse(body);
      console.log(`New order ${order.id} from ${shop}`);
      // Process the order
    },
  });

  if (response.success) {
    console.log(`Webhook registered for ${response.result.topic}`);
  } else {
    console.error(`Failed to register webhook: ${response.result}`);
  }
}

Using the REST API directly:

const axios = require('axios');

async function createWebhook(shop, accessToken, topic, address) {
  const response = await axios.post(
    `https://${shop}/admin/api/2024-01/webhooks.json`,
    {
      webhook: {
        topic: topic,
        address: address,
        format: 'json'
      }
    },
    {
      headers: {
        'X-Shopify-Access-Token': accessToken,
        'Content-Type': 'application/json'
      }
    }
  );

  return response.data.webhook;
}

// Register multiple webhooks
const topics = ['orders/create', 'orders/updated', 'products/update'];
for (const topic of topics) {
  await createWebhook(shop, token, topic, 'https://yourapp.com/webhooks/shopify');
}

Method 2: Shopify Partner Dashboard

For public apps distributed through the Shopify App Store, configure webhooks in your Partner Dashboard under App Setup. This method automatically registers webhooks when merchants install your app.

Navigate to your app in the Partner Dashboard, select "App setup," and add webhook subscriptions. Specify the callback URL and select which topics your app needs. Shopify handles registration during the OAuth installation flow.

Mandatory Webhooks for App Store Apps

If you're building a public Shopify app, certain webhooks are mandatory for App Store approval:

  • customers/data_request: Handle customer data export requests (GDPR)
  • customers/redact: Process customer data deletion requests (GDPR)
  • shop/redact: Clean up shop data when merchants uninstall your app

These compliance webhooks must return a 200 status within specific timeframes, even if processing happens asynchronously.

HMAC Signature Verification

Every Shopify webhook includes an HMAC-SHA256 signature in the X-Shopify-Hmac-Sha256 header. Verifying this signature confirms the webhook originated from Shopify and wasn't tampered with. See our HMAC-SHA256 webhook signatures guide for detailed explanation.

Node.js Verification Implementation

const crypto = require('crypto');

function verifyShopifyWebhook(rawBody, hmacHeader, secret) {
  const generatedHash = crypto
    .createHmac('sha256', secret)
    .update(rawBody, 'utf8')
    .digest('base64');

  return crypto.timingSafeEqual(
    Buffer.from(generatedHash),
    Buffer.from(hmacHeader)
  );
}

// Express.js middleware
const express = require('express');
const app = express();

app.post('/webhooks/shopify',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const hmac = req.headers['x-shopify-hmac-sha256'];
    const secret = process.env.SHOPIFY_WEBHOOK_SECRET;

    if (!verifyShopifyWebhook(req.body, hmac, secret)) {
      console.error('Webhook verification failed');
      return res.status(401).send('Unauthorized');
    }

    // Parse and process the verified webhook
    const data = JSON.parse(req.body.toString());
    const topic = req.headers['x-shopify-topic'];
    const shop = req.headers['x-shopify-shop-domain'];

    console.log(`Received ${topic} from ${shop}`);

    // Acknowledge immediately, process async
    res.status(200).send('OK');

    // Queue for background processing
    processWebhookAsync(topic, shop, data);
  }
);

Critical: Use express.raw() to access the raw request body. If you use express.json(), the body gets parsed and re-serialization won't match the original bytes, causing verification failures.

Complete Verification Middleware

const verifyShopifyWebhookMiddleware = (req, res, next) => {
  const hmac = req.headers['x-shopify-hmac-sha256'];

  if (!hmac) {
    return res.status(401).json({ error: 'Missing HMAC header' });
  }

  const secret = process.env.SHOPIFY_WEBHOOK_SECRET;
  const generatedHash = crypto
    .createHmac('sha256', secret)
    .update(req.body, 'utf8')
    .digest('base64');

  const isValid = crypto.timingSafeEqual(
    Buffer.from(generatedHash),
    Buffer.from(hmac)
  );

  if (!isValid) {
    console.error('HMAC verification failed', {
      shop: req.headers['x-shopify-shop-domain'],
      topic: req.headers['x-shopify-topic']
    });
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Attach parsed data and metadata to request
  req.webhookData = JSON.parse(req.body.toString());
  req.webhookTopic = req.headers['x-shopify-topic'];
  req.webhookShop = req.headers['x-shopify-shop-domain'];

  next();
};

Common Challenges and Solutions

Challenge 1: Webhook Delivery Failures

Shopify expects a 2xx response within 5 seconds. Slow or unavailable endpoints trigger failed delivery and retries. See webhook retry strategies for handling retries gracefully.

Solution: Acknowledge webhooks immediately and process asynchronously.

app.post('/webhooks/shopify', verifyMiddleware, async (req, res) => {
  // Respond immediately
  res.status(200).send('OK');

  // Queue for background processing
  await webhookQueue.add('process-webhook', {
    topic: req.webhookTopic,
    shop: req.webhookShop,
    data: req.webhookData,
    receivedAt: new Date().toISOString()
  });
});

Challenge 2: Duplicate Webhooks

Shopify may send the same webhook multiple times during retries or network issues. Your handlers must be idempotent, especially for order webhooks where duplicates affect inventory or fulfillment. See our webhook idempotency guide for detailed patterns.

Solution: Track processed webhook IDs and skip duplicates.

async function processWebhook(topic, shop, data) {
  const webhookId = `${shop}-${topic}-${data.id}-${data.updated_at}`;

  // Check if already processed
  const exists = await redis.get(`webhook:${webhookId}`);
  if (exists) {
    console.log(`Skipping duplicate webhook: ${webhookId}`);
    return;
  }

  // Mark as processing
  await redis.setex(`webhook:${webhookId}`, 86400, 'processing');

  try {
    // Process the webhook
    await handleWebhookByTopic(topic, data);

    // Mark as completed
    await redis.setex(`webhook:${webhookId}`, 86400, 'completed');
  } catch (error) {
    // Remove lock on failure to allow retry
    await redis.del(`webhook:${webhookId}`);
    throw error;
  }
}

Challenge 3: Rate Limits

Shopify applies rate limits to webhook registrations (not deliveries). Implement backoff strategies when registering webhooks for many stores.

Solution: Batch registrations and respect rate limit headers.

async function registerWebhooksWithRateLimit(shops, topics) {
  for (const shop of shops) {
    for (const topic of topics) {
      try {
        await createWebhook(shop.domain, shop.token, topic, callbackUrl);
      } catch (error) {
        if (error.response?.status === 429) {
          const retryAfter = error.response.headers['retry-after'] || 2;
          await sleep(retryAfter * 1000);
          // Retry once
          await createWebhook(shop.domain, shop.token, topic, callbackUrl);
        } else {
          throw error;
        }
      }
      // Small delay between registrations
      await sleep(100);
    }
  }
}

Challenge 4: Missing Webhooks

Webhooks don't arrive due to endpoint issues, Shopify outages, or misconfiguration. Critical processes shouldn't rely solely on webhooks.

Solution: Implement reconciliation jobs that poll the API periodically.

async function reconcileOrders(shop, accessToken, since) {
  const orders = await fetchOrdersSince(shop, accessToken, since);

  for (const order of orders) {
    const processed = await redis.get(`order:${order.id}`);
    if (!processed) {
      console.log(`Reconciliation found missed order: ${order.id}`);
      await processOrder(order);
    }
  }
}

// Run reconciliation every hour
setInterval(() => {
  const oneHourAgo = new Date(Date.now() - 3600000).toISOString();
  reconcileOrders(shop, token, oneHourAgo);
}, 3600000);

Best Practices for Production

Use Message Queues

Never process webhooks synchronously in the HTTP handler. Use a message queue like Redis, RabbitMQ, or SQS to decouple receipt from processing.

const Queue = require('bull');
const webhookQueue = new Queue('shopify-webhooks', process.env.REDIS_URL);

webhookQueue.process('orders/create', async (job) => {
  const { shop, data } = job.data;
  await syncOrderToERP(shop, data);
  await updateInventory(data.line_items);
  await notifyFulfillment(data);
});

webhookQueue.on('failed', (job, err) => {
  console.error(`Webhook job failed: ${job.id}`, err);
  // Alert on repeated failures
  if (job.attemptsMade >= 3) {
    alertOps(`Webhook processing failed after 3 attempts: ${job.id}`);
  }
});

Implement Comprehensive Logging

Log every webhook with enough context for debugging but avoid logging sensitive customer data.

function logWebhook(topic, shop, data, status) {
  console.log(JSON.stringify({
    timestamp: new Date().toISOString(),
    type: 'webhook',
    topic,
    shop,
    resourceId: data.id,
    status,
    // Don't log PII
  }));
}

Monitor Delivery Health

Track webhook processing metrics: success rates, latency, error types. Set up alerts for anomalies that might indicate integration issues.

Simplify with Hook Mesh

Hook Mesh handles operational overhead—queue infrastructure, retry logic, monitoring, multi-tenant routing. For Shopify integrations specifically:

  • Automatic deduplication based on webhook IDs and timestamps
  • Intelligent routing to different handlers based on topic and shop
  • Failure alerting when delivery issues affect specific stores
  • Replay capability to reprocess webhooks after fixing bugs

Focus on application logic instead of webhook infrastructure.

Conclusion

Production-ready Shopify webhook implementations require careful attention to verification, idempotency, and reliability. Use HMAC verification for every webhook, process asynchronously with message queues, handle duplicates gracefully, and implement reconciliation for critical data.

For multi-channel e-commerce, you may also need payment webhooks from providers like Stripe. For a complete overview, visit our platform integrations hub.

Related Posts