Billing API in Public app in development

export async function loader({ request }) {

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

console.log(“–session—”, session);

let currentPlan = FREE_PLAN;

let buildsUsed = 0;

let appSubscriptions = [];

try {

console.log(“PRO_PLAN:”, PRO_PLAN);

console.log(“MAX_PLAN:”, MAX_PLAN);

console.log(“--------------------------------------”);

console.log(“STARTER_PLAN:”, STARTER_PLAN);

console.log(“IS_TEST_MODE:”, IS_TEST_MODE);

// console.log(“Billing config keys:”, Object.keys(shopify.billing.config));

const billingCheck = await billingApi.check({

plans: [STARTER_PLAN],

isTest: IS_TEST_MODE,

});

appSubscriptions = billingCheck?.appSubscriptions || [];

if (billingCheck?.hasActivePayment && appSubscriptions.length > 0) {

currentPlan = appSubscriptions[0]?.name || FREE_PLAN;

}

} catch (error) {

console.error(“Billing check failed:”, error);

}

return Response.json({

currentPlan,

buildsUsed,

appSubscriptions,

plans: APP_PLANS || [],

});

}

while using the above code in my shopify public app a getting error like
Billing check failed: HttpResponseError: Received an error response (403 Forbidden) from
Shopify:
16:47:18 │ React Router │ {
16:47:18 │ React Router │ “networkStatusCode”: 403,
16:47:18 │ React Router │ “message”: “GraphQL Client: Forbidden”,
16:47:18 │ React Router │ “response”: {}
16:47:18 │ React Router │ }

A 403 with "response": {} is the GraphQL endpoint rejecting the request before it reaches the billing resolver, so the body never tells you which check failed. A few likely causes, in rough order of probability:

1. Plan name doesn’t match your billing config

billingApi.check({ plans: [STARTER_PLAN] }) looks up the plan by name against the keys you registered in shopify.server.js:

import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";

const shopify = shopifyApp({
  billing: {
    [STARTER_PLAN]: {
      amount: 9.99,
      currencyCode: "USD",
      interval: BillingInterval.Every30Days,
    },
  },
});

The key in billing has to match STARTER_PLAN byte-for-byte. A typo, trailing space, or casing mismatch produces a generic 403. Log both side-by-side and compare.

2. App is on Shopify Managed Pricing

If you configured plans in the Partner Dashboard under Distribution > Pricing (the newer recommended path), billingApi.check() won’t work; those plans aren’t visible to the in-code billing API. Query the active plan via currentAppInstallation.activeSubscriptions instead. You can’t mix the two billing models on the same app.

3. Stale or revoked session

A 403 with an empty body is the same signature you’d see if the stored access token has been invalidated, for example after an uninstall/reinstall on the dev store, an API secret rotation, or a scope change that hasn’t been re-authorised. Uninstall from the dev store and reinstall via the app’s install URL to force a fresh OAuth grant.

4. isTest mismatch on a dev store

Confirm IS_TEST_MODE is actually true at runtime (your console.log should print it). Dev stores reject live billing calls, and production stores reject test calls.

One thing worth knowing while you’re in here: the App Billing API itself doesn’t require any extra access scope. write_own_subscription_contracts is for the Subscription Contracts API (merchants selling recurring products to their own customers), which is a separate surface and won’t move this 403.

Next step I’d take: log STARTER_PLAN alongside the keys you’ve registered in the billing block of your shopifyApp config. If they don’t line up, that’s it. If they do, check whether the app is on managed pricing in the Partner Dashboard, which is the second-most common silent cause.

Quick formatting nudge before we dig in. The code’s coming through as separate paragraphs, and the editor is turning the @shopify package names into user mentions, which makes the imports hard to read. If you edit the post, two ways to fix it:

  • Highlight the code in the editor and click the </> “Insert/edit code sample” button in the toolbar, or

  • Wrap the whole block in triple backticks, like:

```js
// shopify.server.js
import "@shopify/shopify-app-react-router/adapters/node";
import {
  ApiVersion,
  AppDistribution,
  BillingInterval,
  shopifyApp,
} from "@shopify/shopify-app-react-router/server";
```

Either one preserves indentation and stops the auto-linking. Once it’s readable I’ll take a proper look.