shopify-webhooks

Register, verify, and reliably process Shopify webhook events for orders, inventory, and customers with HMAC validation and idempotency handling

11 stars

Best use case

shopify-webhooks is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Register, verify, and reliably process Shopify webhook events for orders, inventory, and customers with HMAC validation and idempotency handling

Teams using shopify-webhooks should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/shopify-webhooks/SKILL.md --create-dirs "https://raw.githubusercontent.com/finsilabs/awesome-ecommerce-skills/main/skills/platform-shopify/shopify-webhooks/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/shopify-webhooks/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How shopify-webhooks Compares

Feature / Agentshopify-webhooksStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Register, verify, and reliably process Shopify webhook events for orders, inventory, and customers with HMAC validation and idempotency handling

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Shopify Webhooks

## Overview

Shopify webhooks deliver real-time event notifications to your app's HTTP endpoints when store events occur — orders placed, products updated, customers created, apps uninstalled. Every webhook payload includes an HMAC-SHA256 signature in the `X-Shopify-Hmac-SHA256` header that must be verified before processing. Shopify guarantees at-least-once delivery, so handlers must be idempotent.

## When to Use This Skill

- When triggering fulfillment workflows the moment an order is paid
- When syncing product or inventory changes to an external system in near real time
- When sending customer data to a marketing automation platform upon registration
- When cleaning up app data after a merchant uninstalls the app (`app/uninstalled`)
- When implementing required GDPR webhooks for App Store compliance
- When replacing polling loops that constantly query the Admin API for changes

## Core Instructions

1. **Register webhooks via the Admin API**

   Prefer registering webhooks programmatically in the `afterAuth` hook of your Shopify app. This ensures re-registration after reinstall:

   ```typescript
   // Webhook registration helper
   export async function registerWebhooks(adminClient: GraphqlClient, appUrl: string) {
     const webhooksToRegister = [
       { topic: "ORDERS_CREATE", callbackUrl: `${appUrl}/webhooks/orders-create` },
       { topic: "ORDERS_UPDATED", callbackUrl: `${appUrl}/webhooks/orders-updated` },
       { topic: "PRODUCTS_UPDATE", callbackUrl: `${appUrl}/webhooks/products-update` },
       { topic: "APP_UNINSTALLED", callbackUrl: `${appUrl}/webhooks/app-uninstalled` },
       // Mandatory GDPR webhooks
       { topic: "CUSTOMERS_DATA_REQUEST", callbackUrl: `${appUrl}/webhooks/gdpr/customers-data-request` },
       { topic: "CUSTOMERS_REDACT", callbackUrl: `${appUrl}/webhooks/gdpr/customers-redact` },
       { topic: "SHOP_REDACT", callbackUrl: `${appUrl}/webhooks/gdpr/shop-redact` },
     ];

     for (const { topic, callbackUrl } of webhooksToRegister) {
       const response = await adminClient.request(`
         mutation WebhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {
           webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
             webhookSubscription { id topic }
             userErrors { field message }
           }
         }
       `, {
         variables: {
           topic,
           webhookSubscription: {
             callbackUrl,
             format: "JSON",
           },
         },
       });

       const { userErrors } = response.data.webhookSubscriptionCreate;
       if (userErrors.length > 0) {
         // ALREADY_EXISTS is expected on reinstall — not a real error
         const realErrors = userErrors.filter((e: any) => e.message !== "Address for this topic has already been taken");
         if (realErrors.length > 0) throw new Error(`Webhook registration failed: ${realErrors[0].message}`);
       }
     }
   }
   ```

2. **Verify the HMAC signature**

   The most critical step — never process a webhook without verifying its signature:

   ```typescript
   // middleware/verify-shopify-webhook.ts
   import crypto from "crypto";

   export function verifyShopifyWebhook(
     rawBody: Buffer,
     hmacHeader: string,
     secret: string
   ): boolean {
     const digest = crypto
       .createHmac("sha256", secret)
       .update(rawBody)
       .digest("base64");

     // Use timingSafeEqual to prevent timing attacks
     try {
       return crypto.timingSafeEqual(
         Buffer.from(digest),
         Buffer.from(hmacHeader)
       );
     } catch {
       return false;
     }
   }
   ```

   Express middleware example:

   ```typescript
   // routes/webhooks.ts (Express)
   import express from "express";
   import { verifyShopifyWebhook } from "../middleware/verify-shopify-webhook";

   const router = express.Router();

   // CRITICAL: Use raw body parser BEFORE json parser for webhook routes
   router.use(
     "/webhooks",
     express.raw({ type: "application/json" }),
     (req, res, next) => {
       const hmac = req.headers["x-shopify-hmac-sha256"] as string;
       if (!verifyShopifyWebhook(req.body, hmac, process.env.SHOPIFY_API_SECRET!)) {
         return res.status(401).send("Unauthorized");
       }
       req.body = JSON.parse(req.body.toString());
       next();
     }
   );
   ```

3. **Handle webhook events with idempotency**

   Shopify may deliver the same event multiple times. Use the `X-Shopify-Webhook-Id` header as an idempotency key:

   ```typescript
   router.post("/webhooks/orders-create", async (req, res) => {
     // Respond 200 quickly — Shopify retries if response takes > 5 seconds
     res.status(200).json({ received: true });

     const webhookId = req.headers["x-shopify-webhook-id"] as string;
     const shop = req.headers["x-shopify-shop-domain"] as string;
     const order = req.body;

     // Idempotency check — skip if already processed
     const alreadyProcessed = await db.processedWebhooks.findFirst({
       where: { webhookId, shop },
     });
     if (alreadyProcessed) return;

     // Record processing attempt
     await db.processedWebhooks.create({
       data: { webhookId, shop, topic: "orders/create", processedAt: new Date() },
     });

     // Process the order asynchronously
     await processNewOrder(order, shop);
   });
   ```

4. **Handle the mandatory GDPR webhooks**

   Shopify requires these three endpoints for all App Store apps. They must respond 200 even if your app doesn't store personal data:

   ```typescript
   router.post("/webhooks/gdpr/customers-data-request", async (req, res) => {
     const { shop_id, shop_domain, customer, orders_requested } = req.body;
     // Return customer data your app has stored for this customer
     await sendCustomerDataReport(shop_domain, customer.id);
     res.status(200).json({ received: true });
   });

   router.post("/webhooks/gdpr/customers-redact", async (req, res) => {
     const { shop_domain, customer } = req.body;
     // Delete all personal data for this customer
     await deleteCustomerData(shop_domain, customer.id);
     res.status(200).json({ received: true });
   });

   router.post("/webhooks/gdpr/shop-redact", async (req, res) => {
     const { shop_domain } = req.body;
     // Delete all store data 48 hours after APP_UNINSTALLED
     await deleteShopData(shop_domain);
     res.status(200).json({ received: true });
   });
   ```

5. **Monitor delivery failures and set up retry awareness**

   Shopify retries failed webhooks (non-2xx response or timeout) up to 19 times over 48 hours using exponential backoff. Check delivery health via Admin API:

   ```typescript
   export async function getWebhookFailures(adminClient: GraphqlClient) {
     const response = await adminClient.request(`
       query {
         webhookSubscriptions(first: 20) {
           edges {
             node {
               id
               topic
               callbackUrl
               endpoint {
                 ... on WebhookHttpEndpoint {
                   callbackUrl
                 }
               }
             }
           }
         }
       }
     `);
     return response.data.webhookSubscriptions.edges;
   }
   ```

## Examples

### Full order creation handler with error handling and queue

```typescript
import { Queue, Worker } from "bullmq";

const connection = { host: "localhost", port: 6379 };

const orderQueue = new Queue("order-processing", {
  connection,
  defaultJobOptions: {
    attempts: 3,
    backoff: { type: "exponential", delay: 5000 },
  },
});

router.post("/webhooks/orders-create", async (req, res) => {
  // Must respond within 5 seconds
  res.status(200).json({ received: true });

  const webhookId = req.headers["x-shopify-webhook-id"] as string;
  const shop = req.headers["x-shopify-shop-domain"] as string;

  // Push to queue for reliable async processing
  await orderQueue.add(
    "process-order",
    { order: req.body, shop, webhookId },
    {
      jobId: webhookId, // Prevents duplicate jobs for same webhook
    }
  );
});

const worker = new Worker("order-processing", async (job) => {
  const { order, shop, webhookId } = job.data;
  await syncOrderToERP(order, shop);
  await updateInventoryInWarehouse(order.line_items);
  await sendConfirmationNotification(order);
}, { connection });
```

### List and delete stale webhook subscriptions

```typescript
export async function cleanupWebhooks(adminClient: GraphqlClient, appUrl: string) {
  const response = await adminClient.request(`
    query {
      webhookSubscriptions(first: 100) {
        edges { node { id callbackUrl topic } }
      }
    }
  `);

  const stale = response.data.webhookSubscriptions.edges.filter(
    ({ node }: any) => !node.callbackUrl.startsWith(appUrl)
  );

  for (const { node } of stale) {
    await adminClient.request(`
      mutation DeleteWebhook($id: ID!) {
        webhookSubscriptionDelete(id: $id) {
          deletedWebhookSubscriptionId
          userErrors { field message }
        }
      }
    `, { variables: { id: node.id } });
  }
}
```

## Best Practices

- **Respond 200 within 5 seconds** — offload heavy processing to a background queue (Bull, BullMQ, SQS); Shopify marks slow responses as failures and starts retry cycle
- **Never trust without verifying HMAC** — reject any request that fails signature validation with 401
- **Use raw body for HMAC computation** — any body parsing before HMAC check corrupts the byte representation and causes false signature failures
- **Store `X-Shopify-Webhook-Id` for idempotency** — keep a table of processed webhook IDs to prevent double-processing on retries
- **Re-register webhooks on every OAuth completion** — merchants who reinstall the app get a new session; without re-registration, webhooks point to deleted subscriptions
- **Use `EventBridge` or `Pub/Sub` delivery for high volume** — Shopify supports delivering webhooks to AWS EventBridge and Google Pub/Sub; these provide built-in retry and ordering guarantees

## Common Pitfalls

| Problem | Solution |
|---------|----------|
| HMAC verification always fails | Ensure raw body (`Buffer`) is used — Express's JSON body parser converts Buffer to object; configure raw parser before the JSON parser on webhook routes |
| Webhook events processed twice | Implement idempotency using `X-Shopify-Webhook-Id` as a unique key; Bull `jobId` option prevents duplicate queue entries |
| `APP_UNINSTALLED` not received | Ensure this topic is registered — without it, app cleanup (session deletion, data purge) won't fire and merchant data leaks |
| Shopify stops retrying after 48 hours | Add monitoring to detect gaps in event processing; implement a reconciliation job that queries Admin API for events missed during downtime |
| GDPR webhooks fail Shopify review | All three GDPR endpoints must return `200` within the timeout — even if your app stores no data, acknowledge receipt and log the request |
| Webhook registrations duplicated | Use `webhookSubscriptionUpdate` instead of `webhookSubscriptionCreate` for existing topics, or check for `ALREADY_EXISTS` user errors and skip |

## Related Skills

- @shopify-app-development
- @shopify-admin-api
- @webhook-architecture
- @event-driven-architecture
- @gdpr-compliance

Related Skills

shopify-theme-development

11
from finsilabs/awesome-ecommerce-skills

Build and customize Shopify themes using Liquid templating, JSON sections, dynamic blocks, and theme app extensions for added functionality

shopify-storefront-api

11
from finsilabs/awesome-ecommerce-skills

Build a headless Shopify frontend using the GraphQL Storefront API for product queries, cart management, and checkout with the Buy SDK

shopify-metafields

11
from finsilabs/awesome-ecommerce-skills

Store custom data on any Shopify resource — products, orders, customers — using typed metafield definitions accessible from Liquid and the Storefront API

shopify-checkout-extensions

11
from finsilabs/awesome-ecommerce-skills

Customize Shopify's checkout with UI extensions for upsells and custom fields, plus Shopify Functions for serverless discount and shipping logic

shopify-hydrogen

11
from finsilabs/awesome-ecommerce-skills

Build a custom Shopify storefront using the Hydrogen React framework with Remix routing and deploy it to Shopify's Oxygen edge hosting

wishlist-save-for-later

11
from finsilabs/awesome-ecommerce-skills

Let shoppers save products to a wishlist, share it with friends, and get notified when saved items come back in stock or drop in price

storefront-theming

11
from finsilabs/awesome-ecommerce-skills

Build a themeable storefront with design tokens and CSS custom properties that supports white-labeling, multi-brand variants, and dark mode

search-autocomplete

11
from finsilabs/awesome-ecommerce-skills

Speed up product discovery with instant search suggestions, fuzzy typo matching, and category-aware results powered by Algolia or Elasticsearch

responsive-storefront

11
from finsilabs/awesome-ecommerce-skills

Build a mobile-first storefront with thumb-friendly navigation, sticky add-to-cart buttons, and touch-optimized components for high mobile conversion

recently-viewed-products

11
from finsilabs/awesome-ecommerce-skills

Show shoppers the products they recently browsed using browser storage so they can easily pick up where they left off on your store

quick-view-modal

11
from finsilabs/awesome-ecommerce-skills

Let shoppers preview product details and add items to cart from the listing page without navigating away, reducing friction in the shopping flow

product-page-design

11
from finsilabs/awesome-ecommerce-skills

Design high-converting product detail pages with image galleries, variant selectors, social proof, and clear calls-to-action that drive add-to-cart