All articles
Shopifywebhook debugginglocal testinge-commerce

Test Shopify Webhooks Locally Without Surprises

Most Shopify webhook bugs in local development come down to one line: HMAC is base64-encoded, not hex. If you call .digest('hex') and compare to X-Shopify-Hmac-Sha256, the check will never pass — even with the correct shared secret. We've watched senior engineers lose an afternoon to that one character.

The base64 trap

Stripe uses hex. GitHub uses hex (with an sha256= prefix). Shopify uses base64. Three providers, three encodings. Your first version of the verifier will almost certainly copy a snippet from another project and quietly fail.

Here is what the verification should look like:

const hmac = crypto
  .createHmac('sha256', SHOPIFY_API_SECRET)
  .update(rawBody)         // raw body, not JSON-parsed
  .digest('base64');       // base64, not hex

const valid = crypto.timingSafeEqual(
  Buffer.from(hmac),
  Buffer.from(req.headers['x-shopify-hmac-sha256']),
);

Two things matter beyond the encoding. The body has to be the raw, unparsed bytes. And the comparison has to be timing-safe — string equality leaks information one byte at a time.

Why a tunnel makes this easier

You can also use Shopify's shopify app dev command, which spins up an internal tunnel. It works. But it also wraps your dev server in its own process, swallows logs in a particular way, and gives you no replay button. For app development beyond "hello world", a stable public URL plus a localhost tunnel with request capture saves more time than the CLI saves on initial setup.

npx portpreview 3000

You paste the HTTPS URL into your app's webhook configuration in the Partners dashboard, trigger an event from a test store, and the request lands in your local handler with all headers intact.

Test stores: the part Shopify documentation skims

Two things to know before you wire anything up:

  • Development stores fire all the webhook events. Order creation, fulfillment, inventory — all of it. You don't need to fake anything; just install your app on a dev store and click around.
  • The webhook secret differs per app and per delivery channel. If you also subscribe to EventBridge or Pub/Sub, the HMAC behavior is different. We're talking about plain HTTPS webhook delivery here.

Step-by-step setup

  1. Start your app locally on whatever port you use (3000 is common).
  2. Run npx portpreview 3000 and copy the HTTPS URL it prints.
  3. In the Shopify Partners dashboard, open your app and go to Configuration → Webhooks.
  4. Set the webhook endpoint to https://your-tunnel.portpreview.dev/api/webhooks/shopify (or whatever path your app uses).
  5. Install the app on a dev store, then trigger an event — create a draft order, fulfill an item, change inventory.
  6. Watch the request land. Inspect headers and body. Replay the captured request after each handler fix.

The mistakes we keep seeing

Body parsed before verification

If you put app.use(express.json()) before your webhook route, the raw bytes are gone by the time you try to verify. Mount a raw-body parser only on the webhook path, or pull the body manually from the request stream before any JSON parsing happens.

Mixed up secrets

The Partners app secret is not the same as the storefront API token. The webhook HMAC uses the app secret. If you're staring at shp_xxx in your env file, you grabbed the wrong one.

Test events that look identical

Shopify's "Send test notification" button delivers a synthetic event with a placeholder body. The signature on that test event is real, but the payload is fixed. For realistic payload-shape testing, trigger events from the dev store itself.

Replay is non-negotiable for app dev

Shopify retries failed webhooks for up to 48 hours with exponential backoff. That's generous, but when you're iterating, you don't want to wait for the next retry. Capture the first delivery in your tunnel's request history and replay it on demand against your local handler.

When local stops being enough

Shop registration flows, GDPR webhooks, and subscription billing changes are easier to validate against a deployed environment because they interact with Shopify's own account state. For everything else — payload parsing, signature verification, business logic — local is faster.

For the underlying signature math across all providers, read our webhook signature verification guide. Join the PortPreview waitlist if you want a tunnel with replay built in.

Frequently asked questions

How do I test Shopify webhooks on localhost?
Start a tunnel like PortPreview, get a public HTTPS URL, and register it as the webhook endpoint in the Shopify Partners dashboard. Install your app on a development store, trigger events, and the deliveries reach your local handler.
Why does my Shopify HMAC verification always fail?
Most of the time it's the encoding. Shopify signs with HMAC SHA-256 and base64-encodes the result. If your code uses .digest('hex') instead of .digest('base64'), every comparison fails, even when the secret is correct.
Do I need shopify app dev or can I use any tunnel?
Any tunnel works. shopify app dev includes its own tunnel for convenience, but a general-purpose tunnel with request capture and replay is more useful for sustained development.