Uninstall Webhook Not Triggering on Deployed Remix App (Works Locally & in Staging)

I’m facing an issue with the APP_UNINSTALLED webhook for my Shopify app.

  • My webhook endpoint /webhooks/app/uninstalled works perfectly in local and staging environments.
  • I’m using a Shopify Remix app hosted on Google Cloud Platform (GCP).
  • The webhook is registered properly using shopify.server.js and set to:
APP_UNINSTALLED: {
  deliveryMethod: DeliveryMethod.Http,
  callbackUrl: "/webhooks/app/uninstalled"
}

and this is my webhook with hook code in shopify.server.js file:

webhooks: {
    APP_UNINSTALLED: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/webhooks/app/uninstalled",
      // callback: async (topic, shop, body) => {
      //   await prisma.shops.deleteMany({
      //     where: { myshopify_domain: shop },
      //   });

      //   await ShopTrial.updateOne(
      //     { shop },
      //     { $set: { uninstalledAt: new Date() } },
      //   );
      // },
    },
    APP_SUBSCRIPTIONS_UPDATE: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/webhooks/app/subscriptions",
      callback: async (topic, shop, body) => {
        const payload = JSON.parse(body);
        const subscription = payload.app_subscription;
        if (subscription.status === "CANCELLED") {
          // Clear chargeId on cancellation
          await ShopTrial.updateOne({ shop }, { $set: { chargeId: null } });
        }
      },
    },
  },
  hooks: {
    afterAuth: async ({ session, admin }) => {
      try {
        console.log("afterAuth Hook Triggered");

        // Register necessary webhooks
        await shopify.registerWebhooks({ session });

        // Fetch and save shop data via GraphQL
        await saveShopData(session, admin);
        await setLegacyCustomerMetafield(session.shop, session.accessToken);
        await assignFreePlan(session.shop, session.accessToken);

        await ShopTrial.updateOne(
          { shop: session.shop },
          {
            $setOnInsert: { shop: session.shop, trialUsed: false },
            $set: { uninstalledAt: null },
          },
          { upsert: true },
        );

        console.log("Shop Data Saved Successfully");

        const shop = await Shop.findOne({ myshopify_domain: session.shop });
} catch (error) {
        console.error("Error in afterAuth Hook:", error);
      }
    },
  },

and this is my webhooks.app.uninstall code:

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

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

  if (session) {
    await db.session.deleteMany({ where: { shop } });
  }
 return new Response();
}
  • After deploying the Remix app to GCP, I added the deployed webhook URL to my older Shopify app (which was built using Node.js, React, and ScriptTag).
  • However, when uninstalling the app from a store, the webhook doesn’t seem to trigger.

Has anyone experienced something similar? What could be the reason it’s not working in production but works fine locally and in staging?

Any help would be greatly appreciated! (my app already live, please if any help possible then suggest me)

Hi @drued_rcr

Have you deployed the TOML in the production environment? On prod, you’ll need to also run shopify app dev to push your config so the production app has the webhooks and other TOML managed config in there.

Thank you for you reply @Liam-Shopify

can you please help me understand how to do this in general? The deployment was originally done by a colleague, and I’m not familiar with the setup. If possible, could you please explain the steps in detail.
I’m not sure how to do this — could you please clarify? Do I need to run the shopify app dev command locally in my terminal for the production environment, or is there a different place or method to push the TOML configuration and webhooks to production?

This is my shopify.app.toml:

client_id = "app_client_id"
name = "app name"
handle = "handle_name"
application_url = "https://your_production_url.com"
embedded = true

[build]
automatically_update_urls_on_dev = false
dev_store_url = "storeloot1.myshopify.com"
include_config_on_deploy = true

[access_scopes]
scopes = "read_themes,write_themes"

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

[webhooks]
api_version = "2025-01"

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

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

[pos]
embedded = false
  • Configure your shopify.app.toml for production
  • Run shopify app deploy command to push your configuration to Shopify
  • It’ll show you a diff of what’s being updated (take a screenshot of this incase you’re still running into errors, since this is helpful in debugging)
  • Head over to your Partner Dashboard > Apps > App Name > Configuration to confirm everything was deployed

Now try a fresh install → uninstall loop and see if your webhooks trigger.

Side note, make sure your Shopify CLI is up to date by running shopify --version to check the version or installing the latest version with npm install -g @shopify/cli@latest, incase you run into errors.

yeah, i already do this, but not working, please suggest what should i do?

[quote=“Liam-Shopify, post:2, topic:19799”]
Have you deployed the TOML in the production environment? On prod, you’ll need to also run shopify app dev to push your config so the production app has the webhooks and other TOML managed config in there.

Hey @Liam-Shopify please suggest me hot to do this, my app toml is deployed successfully on partners, i see that inside a versions in configuration and it’s registered successfully, but didn’t work. Please suggest what can i do to resolve this, my app is live but this webhook failed problem occurred continously. This webhook locally and on staging working fine.

hi @Liam-Shopify and @Harshdeep-Shopify i got this error when i use this on live

Webhook authentication failed: Response {
  [Symbol(realm)]: { settingsObject: {} },
  [Symbol(state)]: {
    aborted: false,
    rangeRequested: false,
    timingAllowPassed: false,
    requestIncludesCredentials: false,
    type: 'default',
    status: 400,
    timingInfo: null,
    cacheState: '',
    statusText: 'Bad Request',
    headersList: HeadersList {
      cookies: null,
      [Symbol(headers map)]: Map(0) {},
      [Symbol(headers map sorted)]: null
    },
    urlList: []
  },
  [Symbol(headers)]: HeadersList {
    cookies: null,
    [Symbol(headers map)]: Map(0) {},
    [Symbol(headers map sorted)]: null
  }
}

can you please suggest me how i will resolve this.

hi @JordanFinners
Please help to resolve this:
so i got this error

Webhook authentication failed: Response {
  [Symbol(realm)]: { settingsObject: {} },
  [Symbol(state)]: {
    aborted: false,
    rangeRequested: false,
    timingAllowPassed: false,
    requestIncludesCredentials: false,
    type: 'default',
    status: 400,
    timingInfo: null,
    cacheState: '',
    statusText: 'Bad Request',
    headersList: HeadersList {
      cookies: null,
      [Symbol(headers map)]: Map(0) {},
      [Symbol(headers map sorted)]: null
    },
    urlList: []
  },
  [Symbol(headers)]: HeadersList {
    cookies: null,
    [Symbol(headers map)]: Map(0) {},
    [Symbol(headers map sorted)]: null
  }
}

from my webhook.app.uninstalled.jsx file at this line

try {
      ({ shop, session, topic } = await authenticate.webhook(request));
    } catch (authErr) {
      console.error("Webhook authentication failed:", authErr);
      return new Response("Unauthorized", { status: 401 });
    }" 

i don’t know why this happened on live app. and this is my server.js file

import express from 'express';
import { createRequestHandler } from '@remix-run/express';
import { broadcastDevReady } from '@remix-run/node';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const _dirname = path.dirname(_filename);

const app = express();

// Body parsing middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Serve static files from the build/client directory
app.use(express.static(path.join(__dirname, 'build/client')));

// API routes - ye /api/* routes handle karega
app.use('/api', (await import('./src/api/index.js')).default);

// Remix app ke liye handler
const build = await import('./build/server/index.js');

// Handle all other routes with Remix
app.all('*', createRequestHandler({
  build,
  mode: process.env.NODE_ENV || 'development',
}));

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
  console.log(🚀 Server running on http://localhost:${PORT});

  if (process.env.NODE_ENV === 'development') {
    broadcastDevReady(build);
  }
});

export default app;

Hey,
Please don’t randomly @ people in here, its not good etiquette .

A 400 indicates a bad request, which means it isn’t a webhook authentication issue but something you app is doing.
It looks like you are never responding to the webhook from a skim read of your code.

I’m handling the APP_UNINSTALLED webhook in my Remix + Express app with the following code:

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

This line works fine locally and on staging. However, in my live production app, this line crashes and sends execution to the catch block. As a result, the webhook doesn’t process properly. I’m using @shopify/shopify-app-remix for authentication.

  • Why would authenticate.webhook(request) fail in production but work fine in dev and staging?
  • What are the possible reasons for webhook authentication failing on live?

Here’s the full action handler code I’m using for the uninstall webhook:

import { authenticate } from “../shopify.server”;
import db from “../db.server”;
import { connectDB } from “../../src/api/config/db”;
import { Shop } from “../../src/api/models/shop.model”;
import { ShopTrial } from “../../src/api/models/shopTrial.model”;
import verificationLogModel from “../../src/api/models/verificationLog.model”;
import { Plan } from “../../src/api/models/plan.model”;
import { ToggleButton } from “../../src/api/models/toggle.model”;
import { installEmail } from “../../src/api/utils/mailer”;

export const action = async ({ request }) => {
console.log(“webhook uninstall deletion received”);

try {
await connectDB();

let shop, session, topic;

try {
  ({ shop, session, topic } = await authenticate.webhook(request));
} catch (authErr) {
  console.error("Webhook authentication failed:", authErr);
  return new Response("Unauthorized", { status: 401 });
}

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

if (session) {
  await db.session.deleteMany({ where: { shop } });
}

// Mark shop as uninstalled
const shopDoc = await Shop.findOneAndUpdate(
  { myshopify_domain: shop },
  {
    $set: {
      installStatus: "uninstalled",
      uninstalledAt: new Date(),
    },
  },
);

const existingPlan = await Plan.findOne({ shop });
const trialUsed = !existingPlan || existingPlan.plan !== "Free";

await ShopTrial.updateOne(
  { shop },
  {
    $set: {
      trialUsed,
      chargeId: null,
      uninstalledAt: new Date(),
    },
  },
  { upsert: true },
);

// Clean up
await Plan.deleteMany({ shop });
await verificationLogModel.deleteMany({ shop });
await ToggleButton.deleteMany({ shop });


if (shopDoc?.installStatus === "uninstalled") {
  return new Response();
}

if (shopDoc?.myshopify_domain && shopDoc?.email) {
  const shopDomain = shopDoc.myshopify_domain;
  const email = shopDoc.email;

  await installEmail({
    to: email,
    subject:
      "Thank you for trying our App — Share your feedback",
    html: `html
  `,
  });

  // 2. Auto-reply to company when email is sent to user
  await installEmail({
    to: process.env.SUPPORT_TEAM_EMAIL,
    subject: `🚫 App Uninstalled – ${shopDoc.myshopify_domain}`,
    html: `html`
          });

  console.log("Uninstall feedback email sent to:", email);
}

return new Response();

} catch (error) {
console.error(" Error handling uninstall webhook:", error);
return new Response(“Webhook processing error”, { status: 200 });
}
};

What should I check or log to pinpoint why the webhook is failing only on live?

When you’ve setup your webhooks are you using the correct app credentials, ie. production to set them up?
Are you environment variables set up correctly to your production app?

Please try debugging this yourself as well. Debugging is an incredibly valuable skill, with a little bit of digging.
You know the line the error is coming from, so you can look at the Shopify authenticate method.
This is using the Shopify app package and your environment variables, which hints something might be wrong here. Likely in env vars.
But you can check this out because the shopify app package is available on github, so you can see what the code is doing shopify-app-js/packages/apps/shopify-app-remix/src/server/authenticate/webhooks/authenticate.ts at main · Shopify/shopify-app-js · GitHub