Uninstall webhook not delivering on dev app

Hello all,

I’m having an issue where my app’s uninstall webhook. I’m not sure if it’s because my app isn’t currently public, I copied the implementation of my other public app that I’ve tested and it works on that app. The behavior I’m expecting is a log from my server saying “webhook delivery response 200” to fire when I uninstall my app on my development store. Currently, nothing happens when I uninstall the app. No logs, no updates to my database. It’s as though the webhook is just not firing.

NOTE: If uninstall webhooks don’t fire on development stores or development/non-public apps then that answers my question. If not-

I have a webhook subscription in my TOML file:
[[webhooks.subscriptions]] uri = "https://APP_NAME.herokuapp.com/webhooks" topic = "APP_UNINSTALLED"

I also have this webhook section in my shopify.server.js file:

webhooks: {
    APP_UNINSTALLED: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/webhooks",
    },
    CUSTOMERS_DATA_REQUEST: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/webhooks",
    },
    CUSTOMERS_REDACT: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/webhooks",
    },
    SHOP_REDACT: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/webhooks",
    },
  },
  hooks: {
    afterAuth: async ({ session }) => {
      await shopify.registerWebhooks({ session });
    },
  },
  future: {
    unstable_newEmbeddedAuthStrategy: true,
    removeRest: true,
    v3_webhookAdminContext: true,
  },
  ...(process.env.SHOP_CUSTOM_DOMAIN
    ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }
    : {}),
});
...
export const registerWebhooks = shopify.registerWebhooks;

I tried manually firing an uninstalled webhook and it successfully delivered (so endpoint is correct).

Here’s my webhook file in case it’s that (unlikely, it’s delivering fine)

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

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

    console.log(`Received webhook: ${topic} for shop: ${shop}`);
    console.log('Request headers:', Object.fromEntries(request.headers.entries()));

    if (!admin && topic !== "APP_UNINSTALLED") {
        console.log("No admin context available for non-uninstall webhook");
        throw new Response();
    }

    switch (topic) {
        case "APP_UNINSTALLED":
            console.log("Processing APP_UNINSTALLED webhook");
            try {
                if (session) {
                    console.log("Session found, looking up user");
                    const user = await db.user.findUnique({
                        where: { shopDomain: shop }
                    });

                    if (user?.pixelId) {
                        console.log(`Deleting web pixel with ID: ${user.pixelId}`);
                        await admin.graphql(
                            `mutation {
                                webPixelDelete(id: "${user.pixelId}") {
                                    userErrors {
                                        code
                                        field
                                        message
                                    }
                                }
                            }`
                        );
                    }

                    console.log("Updating user and deleting sessions");
                    await Promise.all([
                        db.session.deleteMany({ where: { shop } }),
                        db.user.update({
                            where: { shopDomain: shop },
                            data: {
                                deletedAt: new Date(),
                                pixelId: null
                            }
                        })
                    ]);
                    console.log("Uninstall cleanup completed successfully");
                }
                return new Response(null, { status: 200 });
            } catch (error) {
                if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {
                    console.log("No records found to delete - this is okay");
                    return new Response(null, { status: 200 });
                } else {
                    console.error("Error during APP_UNINSTALLED webhook:", error);
                    return new Response("Internal server error", { status: 500 });
                }
            }
        case "CUSTOMERS_DATA_REQUEST":
            console.log("Received CUSTOMERS_DATA_REQUEST webhook");
            return new Response("We don't collect customer data.", { status: 200 });
        case "CUSTOMERS_REDACT":
            console.log("Received CUSTOMERS_REDACT webhook");
            return new Response(null, { status: 200 });
        case "SHOP_REDACT":
            console.log("Processing SHOP_REDACT webhook");
            await db.session.deleteMany({ where: { shop } });
            return new Response(null, { status: 200 });
        default:
            console.log(`Received unhandled webhook topic: ${topic}`);
            return new Response("Unhandled webhook topic", { status: 404 });
    }
};

It’s just when I uninstall the app on one of my development stores, it doesn’t fire the uninstalled webhook. I really don’t know what I did to make this not work. Help appreciated!!

I figured out the issue.

For posterity, the issue was that I uninstalled it once and the uninstall webhook presumably ran into an error where it made an invalid prisma call before deleting the user session which made it so the uninstall webhook registered as firing correctly but the session never got deleted. So when I reinstalled the app, it thought that my store never uninstalled the app so it never re-registered the webhooks. The Shopify webhook template calls afterAuth when creating a new session because the logic is if a user doesn’t have a session then they must have just installed the app so it calls the authentication (and afterAuth) flow.

The fix was to uninstall the app and manually delete the user session. Then when I installed it again, it didn’t see a session and realized that my store just installed the app and registered for webhooks.

2 Likes

Hey Beckett! Glad you figured this out and appreciate you posting your solution - that will help devs who encounter this issue in the future!

1 Like

In situations like this, it’s worth checking App Insights under the Monitoring section in Shopify Partners. That’s how I discovered that the app URL had changed after re-running the npm run dev command. Although the new URL was (well partially as it seems) updated in the Partner Dashboard, Shopify was still trying to deliver webhooks to the old URL. The only way I was able to resolve this was by running npm run deploy — note creating a new version via the web UI didn’t help.

I encountered the same issue. The problematic code was generated around November 2024 using:

npm init @shopify/app@latest

At that time, the api_version was set to "2024-10". However, this version of the code has a problem: it does not automatically subscribe to the APP_UNINSTALLED webhook.

If you tried to resolve this issue by referencing older implementations of the app uninstallation webhook, you might have added the following configuration inside shopify.server.ts:

const shopify = shopifyApp({
  apiKey: process.env.SHOPIFY_API_KEY,
  apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
  apiVersion: ApiVersion.July24,
  scopes: process.env.SCOPES?.split(","),
  appUrl: process.env.SHOPIFY_APP_URL || "",
  authPathPrefix: "/auth",
  sessionStorage: new PrismaSessionStorage(prisma),
  distribution: AppDistribution.AppStore,
  restResources,
  webhooks: {
    APP_UNINSTALLED: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/webhooks"
    },
    APP_PURCHASES_ONE_TIME_UPDATE: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/webhooks"
    }
  }
});

Unfortunately, this will not work.

:white_check_mark: The Correct Solution

You need to modify the shopify.app.toml file and add the following:

[webhooks]
api_version = "2024-10"

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

Then, deploy your app using:

npm run shopify app deploy

Additionally, go to your Shopify Partners Dashboard, check the latest active versions, and click on the version to ensure that it includes the app/uninstalled topic.

:warning: Important: The method suggested by Beckett_Oliphant, using topic = "APP_UNINSTALLED", did not work in my case. Instead, the correct approach is:

topics = [ "app/uninstalled" ]

:tada: Good News

This issue has been fixed in the latest versions of Shopify’s app template. If you generate a new project now, it should automatically subscribe to the uninstallation webhook.