The Problem: Shopify Webhooks Won't Hit Your Local Machine
You'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.
This guide walks you through three concrete approaches to test Shopify webhooks locally, from quick manual testing to production-grade inspection.
Prerequisites
- A Shopify Partner account and a development store (free tier works)
- Node.js 16+ installed locally
- A webhook handler running on
localhost:3000(Express, Fastify, or similar) - Familiarity with Shopify Admin API and webhook subscriptions
-
curlor Postman for manual testing
Testing Shopify Webhooks Locally: Three Approaches
Approach 1: Manual Testing with Curl (Fastest for Iteration)
Before 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.
First, grab a real Shopify webhook payload structure. Create a JSON file matching the Shopify order webhook schema:
{
"id": 1234567890,
"email": "test@example.com",
"created_at": "2024-01-15T10:30:00-05:00",
"updated_at": "2024-01-15T10:30:00-05:00",
"number": 1001,
"user_id": null,
"billing_address": {
"first_name": "Test",
"last_name": "Customer",
"phone": "5551234567",
"company": null,
"address1": "123 Main St",
"address2": null,
"city": "Springfield",
"province": "IL",
"country": "United States",
"zip": "62701",
"province_code": "IL",
"country_code": "US"
},
"line_items": [
{
"id": 1,
"variant_id": 1,
"title": "Test Product",
"quantity": 1,
"sku": "TEST-001",
"variant_title": null,
"vendor": "Test Vendor",
"fulfillment_service": "manual",
"product_id": 1,
"requires_shipping": true,
"taxable": true,
"gift_card": false,
"price": "99.99",
"total_discount": "0.00"
}
],
"total_price": "99.99",
"total_tax": "0.00",
"currency": "USD",
"financial_status": "paid",
"fulfillment_status": null,
"tags": "test"
}
Save this as order-webhook.json. Now, start your local webhook handler:
node server.js
# Output: Listening on http://localhost:3000
Send the payload via curl:
curl -X POST http://localhost:3000/webhooks/orders \
-H "Content-Type: application/json" \
-H "X-Shopify-Hmac-SHA256: dummy-signature" \
-d @order-webhook.json
Your 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:
const crypto = require('crypto');
function verifyShopifyWebhook(req, secret) {
if (process.env.SKIP_WEBHOOK_VERIFICATION === 'true') {
return true; // Only in local dev
}
const hmac = req.headers['x-shopify-hmac-sha256'];
const body = req.rawBody; // Store raw body before JSON parsing
const hash = crypto
.createHmac('sha256', secret)
.update(body, 'utf8')
.digest('base64');
return hash === hmac;
}
Set SKIP_WEBHOOK_VERIFICATION=true locally, then test again. You'll see logs from your handler confirming the payload arrived.
Limitation: This approach doesn't test real Shopify signatures or timing. Use it only for rapid iteration on handler logic.
Approach 2: Tunnel-Based Testing (ngrok or Similar)
For real webhook events from Shopify, expose your local server to the internet via a tunnel. ngrok is the most common choice:
ngrok http 3000
ngrok 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.
Now 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.
Trade-offs:
- ✅ Real Shopify signatures and timing
- ❌ URL changes every restart (unless you pay for a static domain)
- ❌ Secrets exposed in terminal history
- ❌ Adds latency; harder to debug network issues
Compare tunnel approaches in detail.
Approach 3: Webhook Relay with Stable Endpoints (Recommended for Debugging)
A 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.
Start your local handler:
node server.js
In another terminal, start the relay:
npx @anonymilyhq/cli listen 3000
The CLI outputs a stable endpoint URL:
✓ Listening on http://localhost:3000
✓ Webhook endpoint: https://api.anonymily.com/h/my-shopify-app
Register 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.
The 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:
# View all captured webhooks
https://api.anonymily.com/h/my-shopify-app
On the Pro tier, you can modify and replay webhooks with re-signed HMAC, helping you debug edge cases without triggering new orders:
# Replay with modified payload (Pro feature)
# Signature is automatically re-signed
This approach is ideal for Shopify order webhook debugging because:
- ✅ Stable endpoint (survives restarts and redeploys)
- ✅ Captures webhooks even when localhost is down
- ✅ Inspect and replay without new orders
- ✅ No secrets in terminal
- ❌ Free tier limited to 200 requests/hook and 48h history
Common Errors and Fixes
Error 1: "Invalid HMAC Signature"
Exact error:
Error: HMAC signature verification failed. Expected: abc123, got: xyz789
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.
Fix:
- Set
SKIP_WEBHOOK_VERIFICATION=trueduring local manual testing. - 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.
// Express example: capture raw body
const express = require('express');
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/orders', (req, res) => {
const rawBody = req.body; // Buffer, not string
const hmac = req.headers['x-shopify-hmac-sha256'];
// Now validate HMAC against rawBody
});
Error 2: "Webhook Endpoint Not Reachable"
Exact error (from Shopify Admin):
Webhook delivery failed: Connection refused. The endpoint may be down or unreachable.
Root cause: Your local handler crashed, or the tunnel/relay isn't running.
Fix:
- Verify your local handler is running:
curl http://localhost:3000/health(if you expose a health check). - If using a tunnel, check that it's still active (ngrok URLs expire after 2 hours of inactivity on free tier).
- If using a relay, verify the CLI is still connected:
npx @anonymilyhq/cli listen 3000should show✓ Connected. - Check Shopify's webhook delivery logs in Admin > Settings > Notifications > Webhooks to see the exact error.
Frequently Asked Questions
Q: Do I need to validate Shopify webhook signatures locally?
A: 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.
Q: Can I test Shopify order webhooks without creating real orders?
A: 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).
Q: What's the difference between ngrok and a webhook relay for debugging Shopify webhooks?
A: 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.
Next Steps
Start 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).
To 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.
Top comments (0)