Webhook request is giving me 401: Unauthorized response

Topic summary

A developer is encountering a 401 Unauthorized error when receiving webhook requests from Shopify for the products/update topic.

Setup Details:

  • Webhook configured for products/update with route /webhooks/app/products/update
  • Multiple webhooks registered including compliance topics (shop/redact, customers/redact, customer data requests) and app/uninstalled
  • API version: 2024-10

Code Implementation:

  • Route handler attempts to authenticate webhook using authenticate.webhook(request)
  • After authentication, code fetches a collection via GraphQL and processes product tags
  • Error occurs in the catch block, showing Response object with status 401

Current Status:
The discussion remains open with no resolution. The developer has shared their webhook configuration (via screenshot) and handler code, but hasn’t identified the root cause. Key questions unanswered:

  • Whether webhook HMAC validation is properly configured
  • If the authentication method matches Shopify’s requirements
  • Whether the endpoint URL is correctly registered and accessible
Summarized with AI on November 4. AI used: claude-sonnet-4-5-20250929.

I have setup a webhook for my clients store for products/update topic. I have setup a route to receive this request. In my catch block, I am getting this error: 401 Unauthorized What Am I doing wrong here?

Response {
04:38:37 │                     remix │   [Symbol(realm)]: { settingsObject: {} },
04:38:37 │                     remix │   [Symbol(state)]: {
04:38:37 │                     remix │     aborted: false,
04:38:37 │                     remix │     rangeRequested: false,
04:38:37 │                     remix │     timingAllowPassed: false,
04:38:37 │                     remix │     requestIncludesCredentials: false,
04:38:37 │                     remix │     type: 'default',
04:38:37 │                     remix │     status: 401,
04:38:37 │                     remix │     timingInfo: null,
04:38:37 │                     remix │     cacheState: '',
04:38:37 │                     remix │     statusText: 'Unauthorized',
04:38:37 │                     remix │     headersList: HeadersList {
04:38:37 │                     remix │       cookies: null,
04:38:37 │                     remix │       [Symbol(headers map)]: Map(0) {},
04:38:37 │                     remix │       [Symbol(headers map sorted)]: null
04:38:37 │                     remix │     },
04:38:37 │                     remix │     urlList: []
04:38:37 │                     remix │   },
04:38:37 │                     remix │   [Symbol(headers)]: HeadersList {
04:38:37 │                     remix │     cookies: null,
04:38:37 │                     remix │     [Symbol(headers map)]: Map(0) {},
04:38:37 │                     remix │     [Symbol(headers map sorted)]: null
04:38:37 │                     remix │   }
04:38:37 │                     remix │ }

This is how I set up my webhook

This is my webhooks.products.jsx file

import prisma from "../db.server";
import { authenticate } from "../shopify.server";

export async function loader() {
  const tag = await prisma.tag.findFirst();
  return { tag };
}

export const action = async ({ request }) => {
  try {
    const { topic, shop, session, admin, payload } =
      await authenticate.webhook(request);
    console.log(topic, shop, session, admin);
    if (!admin) {
      throw new Error("Admin is not defined");
    }

    const firstTag = await prisma.tag.findFirst();
    if (
      firstTag &&
      (firstTag.enabled === "true" || firstTag.enabled === true)
    ) {
      const tags = payload.tags.split(", ");
      // const patternTag = tags.find((tag) => tag.includes("t-"));
      const patternTag = tags.find((tag) => tag.includes(firstTag.pattern));

      if (patternTag) {
        try {
          const collection = await admin.graphql(
            `#graphql
            query {
              collectionByHandle(handle: "shirt") {
                id
                title
              }
            }`,
          );
          console.log(collection);
        } catch (error) {
          console.error("Error fetching collection:", error);
        }
      }
    }
    throw new Response(`We got the request ${request}`, { status: 200 });
  } catch (error) {
    console.error("Error in webhook handler:", error);
    throw new Response("Internal Server Error", { status: 500 });
  }
};

This is my .toml file

[webhooks]
api_version = "2024-10"

  [[webhooks.subscriptions]]
  uri = "/webhooks/customers/data_request"
  compliance_topics = [ "customers/data_request" ]

  [[webhooks.subscriptions]]
  uri = "/webhooks/customers/redact"
  compliance_topics = [ "customers/redact" ]

  [[webhooks.subscriptions]]
  uri = "/webhooks/shop/redact"
  compliance_topics = [ "shop/redact" ]

  [[webhooks.subscriptions]]
  topics = [ "app/uninstalled" ]
  uri = "/webhooks/app/uninstalled"

  [[webhooks.subscriptions]]
  topics = [ "products/update" ]
  uri = "/webhooks/products"