Verifies webhooks with HMAC signatures failure - no requests reaching my app

Hi everyone,

I’m trying to submit my app to the App Store but the automated check “Verifies webhooks with HMAC signatures” consistently fails. I’ve spent days debugging this and I’m stuck.

App Details

  • Framework: Remix with @shopify/shopify-app-remix v3.7.0
  • Hosting: Railway (paid plan)

What’s Passing

  • Immediately authenticates after install
  • Immediately redirects to app UI after authentication
  • Provides mandatory compliance webhooks
  • Uses a valid TLS certificate

What’s Failing

  • Verifies webhooks with HMAC signatures

The Strange Part is No requests appear in my server logs when the HMAC check runs. The check takes 3-5 minutes and then fails, but my logs show zero webhook requests during that time. However:

  • Real webhooks from Shopify work fine (APP_UNINSTALLED returns 200)
  • Manual testing confirms my endpoints return 401 for invalid HMAC
  • My app is accessible (other automated checks pass)

My Implementation

I’m using authenticate.webhook() from @shopify/shopify-app-remix:

 import { authenticate } from "../shopify.server";                                  export const action = async ({ request }) => {                                       try {                                                                             const { topic, shop, payload } = await authenticate.webhook(request);                                              

      // Process webhook...                                                           return new Response("OK", { status: 200 });                                                                        

    } catch (error) {                                                                                                    
     console.error("Webhook authentication failed:", error.message);                return new Response("Unauthorized", { status: 401 });                     
    }                                                                           
  };    

I also created a catch-all /webhooks endpoint with manual HMAC verification:

import crypto from "crypto";                                                                                           
export const action = async ({ request }) => {                                                                         
const rawBody = await request.text();                                                                                
const hmacHeader = request.headers.get("x-shopify-hmac-sha256");                                                     
if (!hmacHeader) {                                                                 return new Response("Unauthorized", { status: 401 });                       }                                                                                                                    
const generatedHash = crypto                                                      .createHmac("sha256", process.env.SHOPIFY_API_SECRET)                           .update(rawBody, "utf8")                                                        .digest("base64");                                                                                                 
const hmacBuffer = Buffer.from(hmacHeader, "base64");                           const generatedBuffer = Buffer.from(generatedHash, "base64");                    const isValid = hmacBuffer.length === generatedBuffer.length &&                 crypto.timingSafeEqual(hmacBuffer, generatedBuffer);                                                 
if (!isValid) {                                                                    return new Response("Unauthorized", { status: 401 });                       }                                                                                                                    
return new Response("OK", { status: 200 });                                                                          
};

My shopify.app.toml

[webhooks]                                                                                                             
api_version = "2025-01"                                                                                                
[[webhooks.subscriptions]]                                                                                             
topics = [ "app/uninstalled" ]                                                  

uri = "/webhooks/app/uninstalled"                                                                                      
[[webhooks.subscriptions]]                                                                                             
topics = [ "app/scopes_update" ]                                                                                       
uri = "/webhooks/app/scopes_update"                                                                                    
[[webhooks.subscriptions]]                                                                                             
uri = "/webhooks/compliance"                                                                                           
compliance_topics = [ "customers/data_request", "customers/redact", "shop/redact" ]   

What I’ve Tried

  • Using authenticate.webhook() from the official Shopify library
  • Manual HMAC verification with crypto
  • Creating a catch-all /webhooks endpoint
  • Returning proper 401 for invalid signatures, 200 for valid
  • Verified my SHOPIFY_API_SECRET matches the dashboard (32 characters)
  • Running shopify app deploy to sync configuration
  • Confirmed app is running and accessible during check

Has anyone else experienced the automated checker not reaching their endpoints? 2. What specific URL does the HMAC checker test? 3. Is there any way to get diagnostic information about why the check failed?

Any help would be greatly appreciated. I’ve seen other threads about this issue but no clear solutions. Thanks!

Hi @mnearents
Can you please share context of your .toml file

# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration

client_id = "5bba010d9fd480c62ee7051e4a0d1dd0"
name = "Ghost Variants"
application_url = "https://app.ghostvariants.com"
embedded = true
handle = "ghost-variants"

[build]
automatically_update_urls_on_dev = false

[webhooks]
api_version = "2025-01"

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

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

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

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

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

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

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

[auth]
redirect_urls = [
  "https://app.ghostvariants.com/auth/callback",
  "https://app.ghostvariants.com/auth/shopify/callback",
  "https://app.ghostvariants.com/api/auth/callback"
]

[app_proxy]
url = "https://app.ghostvariants.com"
subpath = "ghost-variants"
prefix = "apps"

[pos]
embedded = false

@Devsoop shopify.app.toml posted above :index_pointing_up:

If you aren’t seeing any requests in your Railway logs during the check, the automated tester is almost certainly hitting the wrong domain.

Go into your Partner Dashboard → App Setup and manually check the URLs for your GDPR/compliance webhooks. Even if you ran shopify app deploy, I’ve seen those fields get stuck with an old ngrok or cloudflare dev tunnel URL.

The checker specifically tests those GDPR endpoints with a bad HMAC and expects a 401 back. If it’s hitting a dead dev tunnel, it fails the check and you’ll never see the request in your production logs. Double check the main App URL in the dashboard too just to be safe.

@ShopIntegrations Seems like the partner dashboard has changed. I don’t see those URLs there anymore. The only links within my app are Overview, API access requests, Admin performance, Distribution, and App history.

I can see the URLS in my dev dashboard and they are all correct. Everything is pointing to app.ghostvariants.com. Here’s a screenshot.

I can POST to https://app.ghostvariants.com/webhooks/compliance and I get HTTP/2 401 from railway-edge (so the endpoint is reachable and returns the expected failure code). If Shopify’s checker still claims “no requests reaching the app,” it’s probably calling a different URL/app install than what the dev dashboard shows, or the requests are arriving but not being logged (early-return middleware). Add top-of-handler logging for all webhook hits (method/path/user-agent/request-id) and check edge/WAF logs.

curl -i -X POST https://app.ghostvariants.com/webhooks/compliance -d '{}' -H 'Content-Type: application/json'
HTTP/2 401 
content-type: text/plain;charset=UTF-8
date: Wed, 11 Mar 2026 19:15:41 GMT
server: railway-edge
vary: Accept-Encoding
x-railway-edge: railway/us-east4-eqdc4a
x-railway-request-id: dhxSq9eMQQWQ__6yCx5-qw

Unauthorized

@ShopIntegrations Update on debugging

I found a potential root cause - the webhook delivery logs showed requests going to an old Cloudflare dev tunnel URL (mixing-src-passport-host.trycloudflare.com) instead of my production URL.

What I’ve done to fix it:

  • Uninstalled and reinstalled the app on my development store
  • Ran shopify app deploy --reset
  • Confirmed a new app version (fake-variants-30) is now active
  • Verified ALL app versions show the correct production URLs (https://app.ghostvariants.com/…)

Current status:

  • The automated check still fails after waiting several hours
  • No requests appear in my Railway production logs during the check
  • The “Webhook errors” in the dev dashboard doesn’t increment when I run the check (suggesting no webhook requests are even being attempted now)
  • My endpoints are confirmed working - manual tests return 401 for invalid HMAC, 200 for valid

It seems like either:

  • The automated checker is caching the old failure
  • The checker isn’t actually sending requests anymore
  • There’s still a stale URL reference somewhere I haven’t found

Is there a way to fully clear/reset the webhook configuration so the automated checker sees fresh URLs? Or is there a way to see exactly what URL the checker is attempting to reach?

Another issue I see is when I click the “run” button from the partners > manage submission page, I can see Shopify’s automated test stores (appstoretest4, appstoretest9) last installed my app on March 11 at 12:41-12:43 am. I ran shopify app deploy --reset and created a fresh app version after that time. But the automated checker hasn’t reinstalled the app on its test stores since then. It’s reusing the old installations that have stale Cloudflare webhook URLs.

When I click ‘Run’ for the automated checks, no new install/uninstall events appear in the App history. The checker seems to be testing against the old installation state instead of doing a fresh install with the current app version.

Is there a way to force the automated checker to do a fresh install so it picks up the corrected webhook URLs from my latest app version?

If you haven’t already, try running shopify app config push (separate from deploy) to force Shopify to resync the webhook/GDPR URLs from shopify.app.toml. Then hit Run again.

If the appstoretest* shops still don’t reinstall (no new install/uninstall entries), there isn’t a DIY “clear cache” button — you’ll need Partner Support to reset the automated test-store installation so it picks up the latest version/URLs.

@ShopIntegrations push doesn’t exist, did you mean shopify app config use? I tried that and it still failed.

The answer was to create a new app and deploy there. As soon as I did I started seeing logs and it passed hmac. Something in their automated tester got stale and it wasn’t hitting my servers at all even though I fixed the initial issue.