{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreidl7znfw3xv5tzwbxgdp3ltmvkcay7xdhe7s6dsemzaylvlohlnxa",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mp6asucfj4f2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreiaoy7bre3uc4hxgh5fmrys64a5skh2sjxrkywrugpf4ly4mw3feee"
    },
    "mimeType": "image/webp",
    "size": 119998
  },
  "path": "/anonymilyhq/how-to-test-shopify-webhooks-locally-30k3",
  "publishedAt": "2026-06-26T05:38:13.000Z",
  "site": "https://dev.to",
  "tags": [
    "webhooks",
    "api",
    "debugging",
    "devtools",
    "Compare tunnel approaches in detail",
    "See a detailed comparison",
    "anonymily.com",
    "@order-webhook.json",
    "@anonymilyhq"
  ],
  "textContent": "##  The Problem: Shopify Webhooks Won't Hit Your Local Machine\n\nYou're building a Shopify app. Your backend listens on `http://localhost:3000/webhooks/orders`, but when you trigger an order in your test store, nothing arrives. Shopify can't reach your machine—it sits behind a NAT, firewall, or corporate proxy. You need to test Shopify webhooks locally, but setting up ngrok, exposing secrets, or deploying to staging every time you change webhook logic is friction you don't need.\n\nThis guide walks you through three concrete approaches to test Shopify webhooks locally, from quick manual testing to production-grade inspection.\n\n##  Prerequisites\n\n  * A Shopify Partner account and a development store (free tier works)\n  * Node.js 16+ installed locally\n  * A webhook handler running on `localhost:3000` (Express, Fastify, or similar)\n  * Familiarity with Shopify Admin API and webhook subscriptions\n  * `curl` or Postman for manual testing\n\n\n\n##  Testing Shopify Webhooks Locally: Three Approaches\n\n###  Approach 1: Manual Testing with Curl (Fastest for Iteration)\n\nBefore you wire up a tunnel or relay, manually send a Shopify order webhook payload to your handler. This lets you verify signature validation and payload parsing without waiting for a real order event.\n\nFirst, grab a real Shopify webhook payload structure. Create a JSON file matching the Shopify order webhook schema:\n\n\n\n    {\n      \"id\": 1234567890,\n      \"email\": \"test@example.com\",\n      \"created_at\": \"2024-01-15T10:30:00-05:00\",\n      \"updated_at\": \"2024-01-15T10:30:00-05:00\",\n      \"number\": 1001,\n      \"user_id\": null,\n      \"billing_address\": {\n        \"first_name\": \"Test\",\n        \"last_name\": \"Customer\",\n        \"phone\": \"5551234567\",\n        \"company\": null,\n        \"address1\": \"123 Main St\",\n        \"address2\": null,\n        \"city\": \"Springfield\",\n        \"province\": \"IL\",\n        \"country\": \"United States\",\n        \"zip\": \"62701\",\n        \"province_code\": \"IL\",\n        \"country_code\": \"US\"\n      },\n      \"line_items\": [\n        {\n          \"id\": 1,\n          \"variant_id\": 1,\n          \"title\": \"Test Product\",\n          \"quantity\": 1,\n          \"sku\": \"TEST-001\",\n          \"variant_title\": null,\n          \"vendor\": \"Test Vendor\",\n          \"fulfillment_service\": \"manual\",\n          \"product_id\": 1,\n          \"requires_shipping\": true,\n          \"taxable\": true,\n          \"gift_card\": false,\n          \"price\": \"99.99\",\n          \"total_discount\": \"0.00\"\n        }\n      ],\n      \"total_price\": \"99.99\",\n      \"total_tax\": \"0.00\",\n      \"currency\": \"USD\",\n      \"financial_status\": \"paid\",\n      \"fulfillment_status\": null,\n      \"tags\": \"test\"\n    }\n\n\nSave this as `order-webhook.json`. Now, start your local webhook handler:\n\n\n\n    node server.js\n    # Output: Listening on http://localhost:3000\n\n\nSend the payload via curl:\n\n\n\n    curl -X POST http://localhost:3000/webhooks/orders \\\n      -H \"Content-Type: application/json\" \\\n      -H \"X-Shopify-Hmac-SHA256: dummy-signature\" \\\n      -d @order-webhook.json\n\n\nYour handler receives the payload. If you're validating HMAC signatures (which you should), you'll need to skip verification during manual testing or use a test signature. Most Shopify webhook handlers check an environment variable:\n\n\n\n    const crypto = require('crypto');\n\n    function verifyShopifyWebhook(req, secret) {\n      if (process.env.SKIP_WEBHOOK_VERIFICATION === 'true') {\n        return true; // Only in local dev\n      }\n\n      const hmac = req.headers['x-shopify-hmac-sha256'];\n      const body = req.rawBody; // Store raw body before JSON parsing\n      const hash = crypto\n        .createHmac('sha256', secret)\n        .update(body, 'utf8')\n        .digest('base64');\n\n      return hash === hmac;\n    }\n\n\nSet `SKIP_WEBHOOK_VERIFICATION=true` locally, then test again. You'll see logs from your handler confirming the payload arrived.\n\n**Limitation:** This approach doesn't test real Shopify signatures or timing. Use it only for rapid iteration on handler logic.\n\n###  Approach 2: Tunnel-Based Testing (ngrok or Similar)\n\nFor real webhook events from Shopify, expose your local server to the internet via a tunnel. ngrok is the most common choice:\n\n\n\n    ngrok http 3000\n\n\nngrok outputs a public URL like `https://abc123.ngrok.io`. In your Shopify app settings, register your webhook endpoint as `https://abc123.ngrok.io/webhooks/orders`.\n\nNow when you create an order in your test store, Shopify sends a real webhook with a valid HMAC signature. Your handler receives it, validates the signature, and processes the order.\n\n**Trade-offs:**\n\n  * ✅ Real Shopify signatures and timing\n  * ❌ URL changes every restart (unless you pay for a static domain)\n  * ❌ Secrets exposed in terminal history\n  * ❌ Adds latency; harder to debug network issues\n\n\n\nCompare tunnel approaches in detail.\n\n###  Approach 3: Webhook Relay with Stable Endpoints (Recommended for Debugging)\n\nA webhook relay sits between Shopify and your localhost, capturing events in the cloud and forwarding them to your machine over a persistent connection. This survives restarts and lets you inspect, replay, and modify payloads.\n\nStart your local handler:\n\n\n\n    node server.js\n\n\nIn another terminal, start the relay:\n\n\n\n    npx @anonymilyhq/cli listen 3000\n\n\nThe CLI outputs a stable endpoint URL:\n\n\n\n    ✓ Listening on http://localhost:3000\n    ✓ Webhook endpoint: https://api.anonymily.com/h/my-shopify-app\n\n\nRegister `https://api.anonymily.com/h/my-shopify-app` in your Shopify webhook settings. When you trigger an order, the relay captures it and forwards it to your local handler over a Server-Sent Events connection. Your handler processes it normally.\n\nThe relay also persists the webhook in the cloud (48 hours on the free tier). You can inspect the payload, response, and headers in the web dashboard:\n\n\n\n    # View all captured webhooks\n    https://api.anonymily.com/h/my-shopify-app\n\n\nOn the Pro tier, you can modify and replay webhooks with re-signed HMAC, helping you debug edge cases without triggering new orders:\n\n\n\n    # Replay with modified payload (Pro feature)\n    # Signature is automatically re-signed\n\n\nThis approach is ideal for Shopify order webhook debugging because:\n\n  * ✅ Stable endpoint (survives restarts and redeploys)\n  * ✅ Captures webhooks even when localhost is down\n  * ✅ Inspect and replay without new orders\n  * ✅ No secrets in terminal\n  * ❌ Free tier limited to 200 requests/hook and 48h history\n\n\n\n##  Common Errors and Fixes\n\n###  Error 1: \"Invalid HMAC Signature\"\n\n**Exact error:**\n\n\n\n    Error: HMAC signature verification failed. Expected: abc123, got: xyz789\n\n\n**Root cause:** You're manually testing with curl but your handler validates the HMAC. The `X-Shopify-Hmac-SHA256` header you sent doesn't match the body hash.\n\n**Fix:**\n\n  1. Set `SKIP_WEBHOOK_VERIFICATION=true` during local manual testing.\n  2. For real Shopify events, ensure you're reading the _raw request body_ before JSON parsing. Many frameworks parse JSON first, destroying the raw bytes needed for HMAC validation.\n\n\n\n\n    // Express example: capture raw body\n    const express = require('express');\n    const app = express();\n\n    app.use(express.raw({ type: 'application/json' }));\n\n    app.post('/webhooks/orders', (req, res) => {\n      const rawBody = req.body; // Buffer, not string\n      const hmac = req.headers['x-shopify-hmac-sha256'];\n      // Now validate HMAC against rawBody\n    });\n\n\n###  Error 2: \"Webhook Endpoint Not Reachable\"\n\n**Exact error (from Shopify Admin):**\n\n\n\n    Webhook delivery failed: Connection refused. The endpoint may be down or unreachable.\n\n\n**Root cause:** Your local handler crashed, or the tunnel/relay isn't running.\n\n**Fix:**\n\n  1. Verify your local handler is running: `curl http://localhost:3000/health` (if you expose a health check).\n  2. If using a tunnel, check that it's still active (ngrok URLs expire after 2 hours of inactivity on free tier).\n  3. If using a relay, verify the CLI is still connected: `npx @anonymilyhq/cli listen 3000` should show `✓ Connected`.\n  4. Check Shopify's webhook delivery logs in Admin > Settings > Notifications > Webhooks to see the exact error.\n\n\n\n##  Frequently Asked Questions\n\n**Q: Do I need to validate Shopify webhook signatures locally?**\n\nA: Yes, in your handler code. Locally, you can skip verification during rapid iteration by checking an environment variable, but always validate in staging and production. Signature validation ensures the webhook came from Shopify, not an attacker.\n\n**Q: Can I test Shopify order webhooks without creating real orders?**\n\nA: Partially. You can manually send mock payloads via curl to test your handler logic. For real Shopify signatures and timing, you need either a real order (or a test order in your dev store) or a relay that supports provider-signed synthetic events (Anonymily Pro feature).\n\n**Q: What's the difference between ngrok and a webhook relay for debugging Shopify webhooks?**\n\nA: ngrok exposes your local port directly; the URL changes on restart. A relay captures webhooks in the cloud and forwards them to your machine, surviving restarts and letting you inspect/replay payloads. See a detailed comparison.\n\n##  Next Steps\n\nStart with manual curl testing to verify your handler logic. Once that works, use a tunnel or relay to receive real Shopify events. For production, always validate HMAC signatures, log webhook deliveries, and implement idempotency (Shopify may retry failed webhooks).\n\nTo streamline debugging, try `npx @anonymilyhq/cli listen 3000` and register the stable endpoint in Shopify. You'll get webhook inspection and replay without managing tunnels or secrets. Visit anonymily.com to learn more.",
  "title": "How to Test Shopify Webhooks Locally"
}