← All guides
ShopifyJuly 12, 20265 min read

Test Shopify Webhooks on Localhost Without Deploying

Receive a shopify webhook localhost with Relayers: create an endpoint, forward to your dev server, filter with JQ, and keep HMAC verification working.

Shopify only delivers webhooks to public HTTPS URLs. That's a problem the moment you want to build an orders/create handler on your laptop. You end up hardcoding fixtures, deploying half-finished code to staging just to see a real payload, or wrestling with a tunnel that mangles the request body and breaks X-Shopify-Hmac-Sha256 verification.

Relayers gives you a stable public ingest URL, forwards the untouched request to localhost:3000, and lets you filter and transform events with JQ before they hit your code. This walkthrough gets a real Shopify webhook landing on your dev server in a few minutes.

What you'll build

Shopify sends to a public Relayers URL. Relayers routes the event down a WebSocket tunnel to the relayers CLI running on your machine, which forwards it to your local app.

Prerequisites

1Create an endpoint in Relayers

Open the desktop app and go to Endpoints → Create. Name it something like shopify-dev. Relayers gives you a public ingest URL shaped like this:

https://api.relayers.app/v1/webhooks/wep_a1b2c3d4e5

Click Copy URL. You'll paste it into Shopify next. While you're here, hit Send test to confirm the endpoint is live before touching Shopify.

2Register the webhook in Shopify

You have two options depending on whether you're configuring a store or an app.

Store admin (quickest): go to Settings → Notifications → Webhooks, click Create webhook, then:

App config (for custom/public apps): declare the subscription in shopify.app.toml, or register it through the Admin API:

[[webhooks.subscriptions]]
topics = ["orders/create"]
uri = "https://api.relayers.app/v1/webhooks/wep_a1b2c3d4e5"

Common topics you'll wire up: orders/create, orders/paid, products/update, and app/uninstalled.

Every delivery Shopify sends carries X-Shopify-Hmac-Sha256 (a base64 HMAC-SHA256 of the raw body, signed with your app's client secret), plus X-Shopify-Topic and X-Shopify-Shop-Domain. Relayers forwards all of these untouched, which matters in step 5.

3Forward events to localhost

Authenticate the CLI, then start listening:

relayers login
relayers listen --forward localhost:3000

relayers listen opens the tunnel. Now trigger a real event: create a test order in your store (or click Send test in the desktop app). Within a second the payload should hit localhost:3000, with the same body and headers Shopify sent.

Open the Events view in the desktop app to inspect every delivery. The payload inspector shows the raw body and headers, and you can retry any event without re-triggering it in Shopify.

4Route and filter with JQ

You rarely want every order in your local handler. In the Rules tab for your endpoint, add a JQ filter so only paid orders above a threshold get forwarded:

.financial_status == "paid" and (.total_price | tonumber) > 50

Set the rule's destination to the tunnel (localhost:3000). Rules evaluate top to bottom and first match wins, so order them from most specific to most general and drag to reorder. Anything that doesn't match is dropped instead of waking up your dev server.

5Transform the payload

Shopify order payloads are large. Reshape them to exactly what your handler needs in the Transformations tab. Pick jq and map the fields you care about:

{
  order: .id,
  total: .total_price,
  email: .email,
  items: (.line_items | length)
}

The test playground lets you paste a sample order and see the output before you save. Transformations also support JSONata, JavaScript, and Go templates if JQ isn't your thing.

Signatures still verify locally

This is the part most tunnels get wrong. Shopify's HMAC is computed over the raw request body. If anything re-serializes or reformats the JSON in transit, the bytes change and X-Shopify-Hmac-Sha256 no longer matches.

Relayers forwards the original body and headers byte-for-byte. Your local verification code works unchanged:

const crypto = require("crypto");

function verifyShopify(rawBody, hmacHeader, secret) {
  const digest = crypto
    .createHmac("sha256", secret)
    .update(rawBody, "utf8")
    .digest("base64");
  return crypto.timingSafeEqual(
    Buffer.from(digest),
    Buffer.from(hmacHeader)
  );
}

Just make sure your framework hands you the raw body (not a parsed object) before you compute the digest.

Going to production

When you deploy, point Shopify at your real backend URL and keep Relayers in front as your routing and observability layer. The rules and transformations you built locally carry over. Use Export to save your endpoint, rules, and transformations as a JSON bundle, and Import it into another workspace or environment so dev and prod stay in sync.

For production destinations, a rule's target can be a public URL instead of the tunnel, so Relayers can fan the same Shopify event out to multiple services.

Troubleshooting

Wrap-up

You now have real Shopify webhooks landing on localhost:3000, filtered to just the orders you care about, reshaped to your schema, with HMAC verification intact. No staging deploys, no fixture guessing.

Start free at relayers.app and download the desktop app to get your first endpoint running.