Test Shopify Webhooks in Development
Shopify signs every webhook with HMAC-SHA256 using your app secret — the flow differs from Stripe's whsec_ CLI pattern. Use the webhook testing hub to decide tunnel vs other tools first.
Try it in 60 seconds
Shopify delivers webhooks to HTTPS URLs only. Get a public endpoint without deploying:
shopify app dev # or npm run dev
npx localtolink --port 3000
Register https://k7m2x9p4q1.localtolink.com/webhooks/shopify in Partners Dashboard. Trigger a test order in your development store. Check your server logs for the POST — if it arrives, HMAC verification is the next step, not networking.
Shopify webhooks during development
Shopify apps receive webhook events for orders, products, customers, app uninstalls, and GDPR data requests. During local development, your app runs on localhost — unreachable from Shopify's servers. A tunnel gives Shopify a public HTTPS URL to deliver events to your local handler.
This guide covers tunnel setup, webhook registration, HMAC verification, and combining OAuth with webhooks on the same subdomain.
Setup
Terminal 1 — your Shopify app:
shopify app dev
# or: npm run dev
Terminal 2 — tunnel:
npx localtolink --port 3000
Note the assigned subdomain.
Register webhook URL
Depending on your app setup, register webhooks via:
Shopify Partners Dashboard → Your App → Configuration → Webhooks
Admin API — programmatic registration during app setup
shopify.app.toml — for Shopify CLI apps:
[webhooks]
api_version = "2024-10"
[[webhooks.subscriptions]]
topics = ["orders/create"]
uri = "/webhooks/shopify"
With a tunnel, your full webhook URL becomes:
https://k7m2x9p4q1.localtolink.com/webhooks/shopify
Shopify CLI's shopify app dev may manage tunnel URLs automatically in some configurations. When using LocalToLink manually, register the URL in your app configuration.
Verify HMAC signatures
Shopify signs every webhook with HMAC-SHA256 using your app's client secret. Verify before processing:
import crypto from "crypto";
function verifyShopifyWebhook(rawBody, hmacHeader, secret) {
const hash = crypto
.createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("base64");
try {
return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(hmacHeader));
} catch {
return false;
}
}
// Handler
const hmac = req.headers["x-shopify-hmac-sha256"];
if (!verifyShopifyWebhook(rawBody, hmac, process.env.SHOPIFY_CLIENT_SECRET)) {
return new Response("Unauthorized", { status: 401 });
}
Use the raw request body — parsing JSON before verification breaks HMAC validation.
OAuth + webhooks on the same tunnel
Shopify apps typically need both OAuth (install flow) and webhooks. Register the same subdomain for both:
| Setting | Example URL |
|---|---|
| App URL | https://k7m2x9p4q1.localtolink.com |
| Allowed redirection URL | https://k7m2x9p4q1.localtolink.com/auth/callback |
| Webhook URL | https://k7m2x9p4q1.localtolink.com/webhooks/shopify |
When the subdomain changes (new tunnel session), update all three in Partners Dashboard.
See OAuth Callback on Localhost for redirect URI details.
Mandatory compliance webhooks
Shopify requires apps to handle GDPR webhooks:
customers/data_requestcustomers/redactshop/redact
Register these topics and implement handlers before app review. Test delivery through the tunnel the same way as business webhooks.
Common Shopify webhook topics
| Topic | Use case |
|---|---|
orders/create | Fulfillment integrations |
products/update | Catalog sync |
app/uninstalled | Cleanup tenant data |
customers/data_request | GDPR compliance (required) |
shop/redact | GDPR compliance (required) |
Implement compliance handlers even if business logic is incomplete — app review checks for them.
Partners Dashboard vs tunnel subdomain
Shopify signs webhooks with your app secret against the raw request body. Log the body before JSON parsing in development so HMAC mismatches are obvious. After each new npx localtolink session, update both the App URL and webhook endpoints in Partners — install flows and webhooks fail independently if only one field is refreshed. Start from Expose Localhost to the Internet if you need the tunnel basics.
Important notes
- Webhook POSTs are server-to-server; Shopify delivery servers skip the browser visitor interstitial when enabled
- Update URLs when subdomain changes (new session)
- 2-hour session limit — restart tunnel for long sessions
- No request replay — use Shopify Partners webhook delivery logs
- Return 200 quickly — Shopify retries on failure
Troubleshooting
| Problem | Fix |
|---|---|
| HMAC verification fails | Use raw body; confirm client secret matches app |
| Webhook not delivered | Confirm URL in Partners Dashboard matches tunnel subdomain |
| OAuth works, webhooks fail | Separate URL registrations — check both |
| Session ended | Restart tunnel and update URLs |
| App install redirect fails | Update allowed redirection URL with new subdomain |
FAQ
Can I use Shopify CLI's built-in tunnel?
Shopify CLI may provide its own tunnel during shopify app dev. LocalToLink is an alternative when you want a standalone tunnel or CLI tunnel is unavailable.
Do I need a development store?
Yes. Create one in Partners Dashboard for testing installs and webhooks.
What about Shopify Functions?
Functions run on Shopify's infrastructure, not localhost. This guide covers app webhooks only.
See Test Webhooks Locally for the general webhook hub.
Try it now
Start a tunnel from your project directory — no account required.
$ npx --yes localtolink@latest