Webhooks working on test store, but not on real stores

Hi. I launched my app on 6th October, after testing is carefully. I tested that a subscription and instance in my database is created + it is updated on plan changes and everything worked as expected.

Today, I wanted to onboard my first client. I am using the managed pricing, and made a private plan for this store. Yet, after I tested longer, I can’t figure why for this store I don’t get a webhook for the subscription create/update. I get these for my dev store where the test flag is on, but for a real client store I can’t get these webhooks.

shopify.server.ts contains the following:

// Webhook configuration

webhooks: {

APP_SUBSCRIPTIONS_CREATE: {

deliveryMethod: DeliveryMethod.Http,

callbackUrl: “/webhooks/subscriptions”,

},

APP_SUBSCRIPTIONS_UPDATE: {

deliveryMethod: DeliveryMethod.Http,

callbackUrl: “/webhooks/subscriptions”,

},

},

also, my webhooks.subscriptions.tsx is below:

import { type ActionFunctionArgs } from “@remix-run/node”;

import { authenticate } from “../shopify.server”;

import { SubscriptionService } from “../models/subscription.server”;

import db from “../db.server”;

export const action = async ({ request }: ActionFunctionArgs) => {

try {

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

if (![“APP_SUBSCRIPTIONS_CREATE”, “APP_SUBSCRIPTIONS_UPDATE”].includes(topic)) {

return new Response(“Webhook topic not handled”, { status: 404 });

}

const webhookData = payload.app_subscription;

// Get the session for this shop to make GraphQL requests

const sessionRecord = await db.session.findFirst({

where: {

shop,

isOnline: false

  },

});

if (!sessionRecord) {

console.error(`No session found for shop: ${shop}`);

return new Response(“No session found”, { status: 404 });

}

// Fetch complete subscription data from GraphQL API

const SUBSCRIPTION_QUERY = `

  query getSubscription($id: ID!) {

    node(id: $id) {

      ... on AppSubscription {

        id

        name

        status

        test

        trialDays

        currentPeriodEnd

        createdAt

        lineItems {

          plan {

            pricingDetails {

              ... on AppRecurringPricing {

                interval

              }

            }

          }

        }

      }

    }

  }

\`;

const response = await fetch(

`https://${shop}/admin/api/2025-01/graphql.json`,

  {

method: ‘POST’,

headers: {

‘Content-Type’: ‘application/json’,

‘X-Shopify-Access-Token’: sessionRecord.accessToken,

    },

body: JSON.stringify({

query: SUBSCRIPTION_QUERY,

variables: {

id: webhookData.admin_graphql_api_id,

      },

    }),

  }

);

const result = await response.json();

if (result.errors) {

console.error(“GraphQL errors:”, result.errors);

return new Response(“GraphQL query failed”, { status: 500 });

}

const subscriptionData = result.data?.node;

if (!subscriptionData) {

console.error(“Subscription not found in GraphQL response”);

return new Response(“Subscription not found”, { status: 404 });

}

// Check if this is a renewal

const existingSub = await SubscriptionService.getSubscription(shop);

const isRenewal = topic === “APP_SUBSCRIPTIONS_UPDATE” &&

existingSub?.subscription &&

existingSub.subscription.status === “ACTIVE” &&

subscriptionData.status === “ACTIVE” &&

existingSub.subscription.currentPeriodEnd &&

subscriptionData.currentPeriodEnd &&

new Date(subscriptionData.currentPeriodEnd).getTime() !== new Date(existingSub.subscription.currentPeriodEnd).getTime();

console.log("DATA: ", subscriptionData);

// Update subscription in database with REAL data

await SubscriptionService.handleWebhookUpdate(shop, {

id: subscriptionData.id,

name: subscriptionData.name,

status: subscriptionData.status,

currentPeriodEnd: subscriptionData.currentPeriodEnd

});

// Handle subscription states

if (subscriptionData.status === “ACTIVE”) {

if (topic === “APP_SUBSCRIPTIONS_CREATE”) {

console.log(`✨ New subscription created for ${shop}`);

await SubscriptionService.resetUsage(shop);

  } else if (isRenewal) {

console.log(`🔄 Subscription renewed for ${shop} - resetting usage`);

await SubscriptionService.resetUsage(shop);

  } else if (!existingSub || existingSub.subscription?.status !== "ACTIVE") {

console.log(`✅ Subscription activated for ${shop}`);

await SubscriptionService.resetUsage(shop);

  }

} else if (\["CANCELLED", "EXPIRED", "DECLINED", "FROZEN"\].includes(subscriptionData.status)) {

console.log(`🚫 Subscription ended for ${shop} - status: ${subscriptionData.status}`);

await SubscriptionService.disableAccess(shop);

}

return new Response(“Webhook processed successfully”, { status: 200 });

} catch (error) {

console.error(“Webhook processing error:”, error);

return new Response(“Error processing webhook”, { status: 500 });

}

};

To further debug, I made this page in the app:

import { json, type LoaderFunctionArgs } from "@remix-run/node";

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

import { useLoaderData } from "@remix-run/react";




export const loader = async ({ request }: LoaderFunctionArgs) => {

const { admin, session } = await authenticate.admin(request);

// Check webhooks

const webhookResponse = await admin.graphql(`

    {

      webhookSubscriptions(first: 20) {

        edges {

          node {

            id

            topic

            endpoint {

              __typename

              ... on WebhookHttpEndpoint {

                callbackUrl

              }

            }

          }

        }

      }

    }

  `);

const webhookData = await webhookResponse.json();

// Check subscriptions

const subResponse = await admin.graphql(`

    {

      currentAppInstallation {

        activeSubscriptions {

          id

          name

          status

          createdAt

          currentPeriodEnd

        }

      }

    }

  `);

const subData = await subResponse.json();

return json({

shop: session.shop,

webhooks: webhookData.data?.webhookSubscriptions?.edges || [],

subscriptions: subData.data?.currentAppInstallation?.activeSubscriptions || [],

appUrl: process.env.SHOPIFY_APP_URL

  });

};




export default function CheckWebhooks() {

const data = useLoaderData<typeof loader>();

const hasSubWebhooks = data.webhooks.some((w: any) => 

w.node.topic === "APP_SUBSCRIPTIONS_CREATE" || 

w.node.topic === "APP_SUBSCRIPTIONS_UPDATE"

  );

return (

<div style={{ padding: "20px", fontFamily: "monospace", maxWidth: "1200px" }}>

<h1>🔍 Webhook & Subscription Debug</h1>

<div style={{ marginBottom: "30px" }}>

<h2>Shop: {data.shop}</h2>

<p>App URL: {data.appUrl}</p>

</div>

<div style={{ marginBottom: "30px", padding: "20px", background: hasSubWebhooks ? "#d4edda" : "#f8d7da", border: "1px solid", borderColor: hasSubWebhooks ? "#c3e6cb" : "#f5c6cb" }}>

<h3>Status: {hasSubWebhooks ? "✅ Subscription Webhooks ARE Registered" : "❌ Subscription Webhooks NOT Found"}</h3>

</div>

<div style={{ marginBottom: "30px" }}>

<h3>📋 All Registered Webhooks ({data.webhooks.length}):</h3>

<pre style={{ background: "#f5f5f5", padding: "15px", overflow: "auto" }}>

{JSON.stringify(data.webhooks, null, 2)}

</pre>

</div>

<div>

<h3>💳 Active Subscriptions ({data.subscriptions.length}):</h3>

<pre style={{ background: "#f5f5f5", padding: "15px", overflow: "auto" }}>

{JSON.stringify(data.subscriptions, null, 2)}

</pre>

</div>

</div>

  );

}

This confirms that I have an active subscription, but no webhook is registered. So I don’t receive the SUBSCRIPTIONS_CREATED webhook and that’s is why the entry in the database is not created, which makes the app usage impossible.

Can someone assist me please on how to correctly register the webhooks?

Guys, feels a bit frustrating to answer threads created 5 mins ago, and I still get no answer while a client waits for me…

Hey @Fabian_Jichi1999 - I would recommend specifying your webhook configuration inside your app.toml, such that it’s Shopify managed.

Where in your app are you currently calling the webhook creation API? It looks like it either failed to register, or it was never called in the first place.

Your example webhook code, suggests this you’re adding shop-specific webhooks is in the shopifyApp call, if you do so, you do need to also call shopify.registerWebhooks({ session }); in your afterAuth hook. This DOES mean that the webhook registration only happens….after authentication, which may have already been called for your merchant if they had previously installed the application.

One thing you could try, if you don’t want to use app-specific webhooks, is the remove the session for that merchant from your database - which will force them to go through afterAuth again (and thus register the webhook).

Hope that helps! I understand your frustration in getting slow responses. My guess is that this query isn’t in the best category (app-store) - @Liam-Shopify - which category do you think this query fits best?

I’ve just moved this to the Webhooks and Events category :wink:

1 Like