Shopify Webhooks to Localhost: The Definitive Node.js Guide
Shopify webhooks are notoriously difficult to test locally due to strict timeouts. FetchHook acts as a persistent buffer, allowing you to debug Shopify apps without a tunnel.
Shopify Fetch Primitive (Node.js)
const fetchEvents = async () => {
const res = await fetch("https://api.fetchhook.app/api/v1/stash_shopify_dev", {
headers: { "Authorization": "Bearer fh_live_xxx" }
});
const { events } = await res.json();
for (const event of events) {
console.log(`Processing Shopify order: ${event.payload.name}`);
await processOrder(event.payload);
}
};#Why do Shopify webhooks fail on localhost?
Shopify expects an HTTP 200 response within 5-10 seconds and has very aggressive timeout and retry policies. If your local Node.js process is busy (processing another webhook, running a build, etc.) or your tunnel is slow to wake up, Shopify will timeout. After 3 consecutive failures, Shopify automatically disables your webhook endpoint. You'll stop receiving events entirely until you manually re-enable in the Shopify admin. This makes local development frustrating—one slow response breaks everything.
#How does FetchHook solve the timeout problem?
FetchHook provides an always-on ingress that responds to Shopify in <100ms, regardless of your local environment's state. Shopify sees instant success (HTTP 202), the webhook is marked as delivered, and FetchHook stores it in your mailbox for 24 hours. Your local script pulls events on its own schedule—when you're ready, when your dev server is running, when you have time to debug. No timeout pressure, no disabled endpoints, no lost orders.
The Timeout Problem Solved
Traditional (ngrok):
Shopify sends webhook → ngrok tunnel (500ms) → localhost (busy, 15s)
→ Shopify timeout @ 10s → Retry #1 → Timeout → Retry #2 → Timeout
→ Shopify disables endpoint → All future webhooks lost
FetchHook:
Shopify sends webhook → FetchHook (50ms response) → HTTP 202 success
→ Shopify happy, marks as delivered
→ Webhook stored in mailbox for 24 hours
→ Your script pulls when ready (no timeout pressure)
→ Process at your own pace#How do I configure Shopify to send webhooks to FetchHook?
Configuration is simple: (1) Go to Shopify Admin → Settings → Notifications → Webhooks (or for custom apps: Apps → Your App → Configuration), (2) Create webhook subscription, (3) Event: Select event type (Order creation, Product update, etc.), (4) Format: JSON, (5) URL: https://api.fetchhook.app/in/stash_shopify_dev, (6) API version: Latest (2024-10 or newer). Save and Shopify will immediately start sending events to FetchHook. Copy your webhook signing secret for local verification (optional, FetchHook verifies it).
Shopify Admin Configuration
Shopify Admin → Settings → Notifications → Webhooks
Event: Order creation
Format: JSON
URL: https://api.fetchhook.app/in/stash_shopify_dev
Shopify API Version: 2024-10
After saving, test by creating a test order.
Check FetchHook mailbox:
curl https://api.fetchhook.app/api/v1/stash_shopify_dev \
-H "Authorization: Bearer fh_live_xxx"#How does Shopify signature verification work?
Instead of writing complex HMAC-SHA256 verification logic in your local script, FetchHook verifies the 'X-Shopify-Hmac-Sha256' header for you at the edge. Shopify signs webhooks with a base64-encoded HMAC using your shared secret. Verifying this requires raw body access and crypto libraries. FetchHook handles it automatically—you only need to check 'signature_verified' is true before processing orders. This prevents fake orders from malicious actors.
Processing with Signature Verification
const axios = require('axios');
const FETCHHOOK_API_KEY = process.env.FETCHHOOK_API_KEY;
const SOURCE_ID = 'stash_shopify_dev';
async function processShopifyOrders() {
try {
const response = await axios.get(
`https://api.fetchhook.app/api/v1/${SOURCE_ID}`,
{ headers: { Authorization: `Bearer ${FETCHHOOK_API_KEY}` } }
);
const events = response.data.events || [];
for (const event of events) {
// CRITICAL: Verify signature before processing orders
if (!event.signature_verified) {
console.warn(`⚠️ Unverified event rejected: ${event.id}`);
continue;
}
// Safe to process - cryptographically verified
const order = event.payload;
console.log(`✓ Processing order: ${order.name}`);
console.log(` Customer: ${order.customer?.email}`);
console.log(` Total: ${order.total_price} ${order.currency}`);
// Your order processing logic
await fulfillShopifyOrder(order);
}
console.log(`Processed ${events.length} Shopify orders`);
} catch (error) {
console.error('Failed to process orders:', error.message);
}
}
processShopifyOrders();#Which Shopify webhook events should I listen for?
Common Shopify webhooks for e-commerce apps: (1) orders/create - New order placed, send to fulfillment, (2) orders/updated - Order modified, sync inventory, (3) orders/fulfilled - Order shipped, send tracking to customer, (4) orders/cancelled - Order cancelled, issue refund, (5) products/create & products/update - Sync product catalog, (6) customers/create - New customer onboarding, (7) refunds/create - Process refund, update accounting. For inventory management apps, add inventory_levels/update. For analytics apps, add checkout/create.
Event Handler Pattern
// Event handler map
const shopifyHandlers = {
'orders/create': async (order) => {
console.log(`New order: ${order.name}`);
await sendToFulfillmentCenter(order);
await sendOrderConfirmationEmail(order);
},
'orders/fulfilled': async (order) => {
console.log(`Order fulfilled: ${order.name}`);
await sendShippingNotification(order);
},
'orders/cancelled': async (order) => {
console.log(`Order cancelled: ${order.name}`);
await processRefund(order);
await updateInventory(order.line_items);
},
'products/update': async (product) => {
console.log(`Product updated: ${product.title}`);
await syncProductToDatabase(product);
}
};
async function processShopifyEvent(event) {
// Event topic is in the X-Shopify-Topic header or inferred
const topic = event.metadata?.topic || inferTopic(event.payload);
const handler = shopifyHandlers[topic];
if (handler) {
await handler(event.payload);
} else {
console.log(`Unhandled Shopify event: ${topic}`);
}
}#How do I test Shopify webhooks in development?
Testing is easy: (1) Create test orders in your Shopify development store, (2) Or use Shopify's webhook testing tool (Admin → Settings → Notifications → Send test notification), (3) Or trigger events via Shopify GraphQL Admin API. All webhooks go to FetchHook. Your local Node.js script pulls and processes them. You can restart your dev server as many times as needed—events wait in the mailbox. You can even batch-test by triggering 10 webhooks, then pulling them all at once to test batch processing logic.
Local Development Loop
# Terminal 1: Run your processing script
node process_shopify.js
# Terminal 2: Create test order in Shopify admin
# Or use Shopify CLI to trigger webhooks
shopify webhook trigger --topic=orders/create
# Your script automatically pulls and processes
# No ngrok, no tunnel, no URL updates#How do I deploy this to production?
For production: (1) Create a separate FetchHook stash (stash_shopify_prod), (2) Configure Shopify production store to send webhooks to the prod stash URL, (3) Deploy your processor as a serverless function (AWS Lambda, Google Cloud Run) triggered by scheduler every 1-5 minutes, or as a background worker in your existing infrastructure. Use environment variables for API keys. For high-volume stores (>1000 orders/day), consider running multiple workers polling the same mailbox in parallel for faster processing.
Production AWS Lambda Deployment
// AWS Lambda handler (triggered by EventBridge every 2 minutes)
exports.handler = async (event) => {
const axios = require('axios');
const response = await axios.get(
`https://api.fetchhook.app/api/v1/${process.env.SOURCE_ID}`,
{ headers: { Authorization: `Bearer ${process.env.FETCHHOOK_API_KEY}` } }
);
const orders = response.data.events || [];
console.log(`Processing ${orders.length} Shopify orders`);
for (const event of orders) {
if (event.signature_verified) {
await processShopifyOrder(event.payload);
}
}
return { statusCode: 200, body: `Processed ${orders.length} orders` };
};
// Environment variables:
// SOURCE_ID=stash_shopify_prod
// FETCHHOOK_API_KEY=fh_prod_xxx
// SHOPIFY_API_KEY=shpat_xxx#How do I monitor Shopify webhook health?
Monitor webhook health from both sides: (1) Shopify Admin: Settings → Notifications → Webhooks shows delivery success rate and recent failures, (2) Your logs: Track events pulled per run, processing success/failure rate, signature verification failures. Set up alerts for: Shopify delivery failures (endpoint might be misconfigured), No events pulled in 24 hours (webhook might be disabled or no activity), High processing error rate (bug in handler code). Shopify will email you if webhooks are auto-disabled due to failures.
Agent Protocol Instruction