Why is the billing page on my app continuously looping?

Topic summary

Billing page repeatedly loops during Shopify Apps review, though it works in development. The app uses an Express middleware (ensureBillingMiddleware) to verify billing before handling /api/* routes.

Key logic: it checks active payment via shopify.api.billing.check against configured plans. If no payment, it requests a confirmationUrl (billing request) and responds 403 with Shopify reauthorization headers (X-Shopify-API-Request-Failure-Reauthorize: 1 and X-Shopify-API-Request-Failure-Reauthorize-Url: confirmationUrl), then ends the response.

Technical notes: middleware intercepts requests; the isTest flag is set based on ENV (isTest = !isProdOverride), which controls test vs real charges; confirmationUrl is the billing approval link.

Current status: multiple developers report the same looping behavior during review; no resolution or workaround is provided in the thread. No decisions or action items were reached.

Open questions: what triggers the loop under review conditions and how to prevent it; whether environment/test mode or header handling causes repeated redirects.

Central artifact: the provided code snippet is essential to understanding the issue.

Summarized with AI on January 15. AI used: gpt-5.

Hi there,

Everything works on dev but when Shopify Apps team tested my app, they are stuck in the billing page (it keeps looping again and again).

Here’s my ensure billing middleware:

import express from "express";
import shopify from "../shopify.js";

export async function ensureBillingMiddleware(
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) {
  const session = res.locals.shopify.session
  const result = await ensureBilling(session)

  if (result.paymentUrl) {
    res.status(403);
    res.header("X-Shopify-API-Request-Failure-Reauthorize", "1");
    res.header("X-Shopify-API-Request-Failure-Reauthorize-Url", result.paymentUrl);
    res.end();
    return
  }

  next()
}

async function ensureBilling(
  session: any,
  isProdOverride = process.env.ENV === 'production'
) {
  let hasPayment = true;
  let confirmationUrl: string | null = null;

  if (shopify.api.config.billing) {
    hasPayment = await shopify.api.billing.check({
      session,
      plans: Object.keys(shopify.api.config.billing),
      isTest: !isProdOverride,
    });

    if (!hasPayment) {
      // Realistically, if there are more than one plan to choose from, you should redirect to
      // a page that allows the merchant to choose a plan.
      // For this example, we'll just redirect to the first plan
      confirmationUrl = await shopify.api.billing.request({
        session,
        plan: Object.keys(shopify.api.config.billing)[0],
        isTest: !isProdOverride,
      });
    }
  }

  return { hasPayment: hasPayment, paymentUrl: confirmationUrl };
} 
app.use("/api/*", ensureBillingMiddleware)

Anyone faced the same issue?

Thanks!

2 Likes

I am having the same issues, did you manage to find a resolution?

Hi @yadsgroup - did you ever find a solution for this? Currently experiencing the same problem.

1 Like