Webhook request is not valid | missing_hmac

I have a basic remix app, using the standard setup from the example app. Yesterday, all of my webhook requests started failing validation. I’m using the helper function for authentication. This is the code of one webhook:

import type { ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";

export const action = async ({ request }: ActionFunctionArgs) => {
  const { shop, session, topic } = await authenticate.webhook(request);

  console.log(`Received ${topic} webhook for ${shop}`);

  if (session) {
    // hanlde topic
  }

  return new Response();
};

I enabled debug logging:

const shopify = shopifyApp({
  ...
  logger: {
    level: LogSeverity.Debug,
  },
  ...
});

This is what is logged when a webhook request comes in:

[shopify-api/DEBUG] webhook request is not valid | {reason: missing_hmac}
[shopify-app/DEBUG] Webhook validation failed | {valid: false, reason: missing_hmac}

The header is there when I check the header in the webhook:

request.headers.get("x-shopify-hmac-sha256");

Does anyone have any ideas why I’m getting this error? It was working two days ago.

Hello @nathan_o

This is a classic issue when handling Shopify webhooks in Node.js. The “missing hmac” error is misleading; the real problem is that your HMAC validation is failing because a body-parser middleware (like express.json()) is altering the request body before your verification function can access it.

Shopify’s validation requires the raw, unparsed request body to generate a matching signature, but by default, Express modifies req.body into a parsed JSON object first, causing the mismatch.

The correct solution is to capture the raw body before it gets parsed. You can do this by adding a verify function to your express.json() middleware, which saves the raw buffer to a new property like req.rawBody. Your validation function must then use this raw body for the HMAC comparison. Only after a successful verification should you parse the body for your own use.

You can try this out:

const express = require('express');
const crypto = require('crypto');
const app = express();

// A function to verify the webhook using the raw body
const verifyWebhook = (req, res, next) => {
  const hmac = req.get('X- Shopify-Hmac-Sha256');
  const body = req.rawBody; // Use the rawBody, not req.body
  const shopifySecret = 'YOUR_SHOPIFY_APP_SECRET'; // Replace with your secret

  const generatedHash = crypto
    .createHmac('sha256', shopifySecret)
    .update(body, 'utf8')
    .digest('base64');

  if (crypto.timingSafeEqual(Buffer.from(generatedHash), Buffer.from(hmac))) {
    req.body = JSON.parse(body); // Now parse the body for your own use
    next();
  } else {
    res.status(401).send('Could not verify webhook');
  }
};

// Configure express.json() to save the raw body before parsing
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf;
  }
}));

// Your webhook route, with the verification middleware
app.post('/webhooks/orders/create', verifyWebhook, (req, res) => {
  // If we get here, the webhook is verified.
  console.log('🎉 Webhook received and verified:', req.body);
  res.status(200).send('OK');
});

app.listen(3000, () => console.log('Server is listening on port 3000'));

This code correctly sets up the middleware, uses req.rawBody for the critical validation step, and then makes the parsed JSON available to your final route handler.

Hope this helps!

Thanks for your response. As you can see from my description, I’m not using Express. I’m using the basic, out of the box Remix app, and don’t have any middleware set up. It had been working without a problem. This is the webhook:

import type { ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";

export const action = async ({ request }: ActionFunctionArgs) => {
  const { shop, session, topic } = await authenticate.webhook(request);

  console.log(`Received ${topic} webhook for ${shop}`);

  if (session) {
    // hanlde topic
  }

  return new Response();
};

This is failing at:

const { shop, session, topic } = await authenticate.webhook(request);

I’ve been able to implement my own validation routine, but it would be nice to rely on the standard implementation from Shopify’s library.