Shopify signs every webhook with HMAC-SHA256(secret, raw body),
base64-encoded, in the X-Shopify-Hmac-Sha256 header. A classic
integration bug is verifying against a hex digest, or against re-serialized JSON — both
look right and both fail. Easiest way to get it right: capture a real delivery first.
$ curl https://catchhook.catchhook.workers.dev/new
Admin → Settings → Notifications → Webhooks → Create webhook (or via the
Admin API / your app's webhook subscription). Pick an event like Order creation and
paste your https://catchhook.catchhook.workers.dev/h/… URL. From the same screen Shopify lets you
send a test notification — do that.
Open your bin dashboard: the capture shows the full JSON payload plus headers like
X-Shopify-Topic, X-Shopify-Shop-Domain,
X-Shopify-Webhook-Id and X-Shopify-Hmac-Sha256.
In the bin's settings, choose the Generic HMAC-SHA256 signature scheme, set the
header name to x-shopify-hmac-sha256, and paste your secret — the webhook signing
secret shown in the notifications settings, or your app's API secret key for
app-subscription webhooks. Every capture now gets a ✓ sig / ✗ sig
badge; CatchHook accepts hex or base64 digests, so Shopify's base64 header verifies as-is.
Reference implementation for your handler (Node):
import crypto from 'node:crypto';
const digest = crypto.createHmac('sha256', secret)
.update(rawBody).digest('base64');
const ok = crypto.timingSafeEqual(Buffer.from(digest),
Buffer.from(req.headers['x-shopify-hmac-sha256']));
Gotcha: HMAC the raw request bytes. If your framework parses JSON before
you can read the body (Express's express.json() without a verify
callback), the re-stringified body won't match. And it's digest('base64'), not
('hex').
Use copy as code on any capture to reproduce it against your local handler with the original headers and body, or Replay it to your real endpoint once deployed. The replayed body is byte-identical, so your HMAC check still verifies.
No signup needed. Or from your terminal: curl https://catchhook.catchhook.workers.dev/new