Failing HMAC Validation in Partner Network for Shopify Remix Template

Hi there!

I am currently trying to develop a small app to help with address validation. The app is embedded into Shopify Admin where it shows some small information on how to use and uses a “shopify functions” extension to validate the address field in checkout. I also used the shopify CLI with the remix app template to create the app and installed it inside my dev store.

So far i implemented the GDPR webhooks, which are accecpted by store tests and am stuggling now on HMAC implementation. I mainly followed these two guides:

  1. How to configure GDPR Compliance webhooks in Shopify Public Apps. | by Muhammad Ehsanullah | Medium
  2. How do I implement HMAC signature for webhook verification in a Remix-Run app? - #5 by FyRo

This is my current status:

Starting with the first guide i implemented the webhooks.

from my shopify.app.toml:

[build]
automatically_update_urls_on_dev = true

[webhooks]
api_version = "2025-07"

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

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

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

[access_scopes]
# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
scopes = "write_products"

[auth]
redirect_urls = ["https://painted-muslim-immediately-machines.trycloudflare.com/auth/callback", "https://painted-muslim-immediately-machines.trycloudflare.com/auth/shopify/callback", "https://painted-muslim-immediately-machines.trycloudflare.com/api/auth/callback"]

[pos]
embedded = false

Then in /app/routes i created a webhooks.tsx

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

export const action = async ({ request }: ActionFunctionArgs) => { 
  if (request.method !== "POST") {
    return new Response("Method not allowed", { status: 405 });
  } 
  
  try {  
    // Clone the request before passing it to authenticate.webhook()
    const requestClone = request.clone();

    const trustworthy = await verifyWebhook(requestClone);
    console.log("TRUSTWORTHY WEBHOOK: %o", trustworthy);
    if (!trustworthy) return new Response("Invalid HMAC", { status: 401 });

    // Authenticate the webhook request
    const { shop, payload, topic } = await authenticate.webhook(request);

    console.log(`Received ${topic} webhook from ${shop}`);
    console.log("Payload:", JSON.stringify(payload, null, 2));

    switch (topic) {      

      case "CUSTOMERS_DATA_REQUEST":
        //Logic for requesting custoers data goes here...
        console.log(`📌 No customer data stored. Responding to data request for shop: ${shop}`);
        break;

      case "CUSTOMERS_REDACT":
        //Logic for removing customer data goes here...
        console.log(`📌 No customer data stored. Ignoring redaction request for shop: ${shop}`);
        break;

      case "SHOP_REDACT":
        //Logic for removing shop data goes here...
        console.log(`📌 No shop data stored. Acknowledging shop deletion request for shop: ${shop}`);
        break;

      default:
        console.warn(`❌ Unhandled webhook topic: ${topic}`);
        return new Response("Unhandled webhook topic", { status: 400 });
    }

    // Return 200 only if authentication and processing succeeded
    return new Response("Webhook received", { status: 200 });

  } catch (authError) {
    // Specifically handle HMAC validation failures
    console.error("🔒 Webhook authentication failed:", authError);
    return new Response("Webhook HMAC validation failed", { status: 401 });
 }
  
};

This got the GPDR webhooks green!

And finally in /app i created verifyWebhook.ts

import crypto from "node:crypto";

export const verifyWebhook = async (request: Request) => {
  const incomingHmac = request.headers.get("x-shopify-hmac-sha256");
  if (!incomingHmac) return false;

  const secret = process.env.SHOPIFY_API_SECRET;
  if (!secret) {
    console.warn("Missing SHOPIFY_API_SECRET");
    return false;
  }

  // Use raw bytes (not text) to avoid encoding issues
  const raw = Buffer.from(await request.arrayBuffer());

  const computed = crypto.createHmac("sha256", secret).update(raw).digest(); // Buffer
  let received: Buffer;
  try {
    received = Buffer.from(incomingHmac, "base64");
  } catch {
    return false;
  }

  const hmacValid = crypto.timingSafeEqual(computed, received);
  return hmacValid;
};

Now i went back to the shopify cli for testing.

after running

shopify app webhook trigger --topic customers/redact

i got

11:13:01 │                     remix │ TRUSTWORTHY WEBHOOK: true
11:13:01 │                     remix │ Received CUSTOMERS_REDACT webhook from shop.myshopify.com
11:13:01 │                     remix │ Payload: {
11:13:01 │                     remix │   "shop_id": 954889,
11:13:01 │                     remix │   "shop_domain": "{shop}.myshopify.com",
11:13:01 │                     remix │   "customer": {
11:13:01 │                     remix │     "id": 191167,
11:13:01 │                     remix │     "email": "john@example.com",
11:13:01 │                     remix │     "phone": "555-625-1199"
11:13:01 │                     remix │   },
11:13:01 │                     remix │   "orders_to_redact": [
11:13:01 │                     remix │     299938,
11:13:01 │                     remix │     280263,
11:13:01 │                     remix │     220458
11:13:01 │                     remix │   ]
11:13:01 │                     remix │ }
11:13:01 │                     remix │ 📌 No customer data stored. Ignoring redaction request for shop: shop.myshopify.com

in my shopify app dev terminal. And now here i am kinda stuck. What am i missing? Do i need to set up a deticated domain with TSL first? Did i implement the hmac validation incorrectly?

Any help would be appreciated since theres so much different information about this topic floating around. Thank you!

Best regards

jch4nni