Context-
I am building a public Shopify app using:
-
@shopify/shopify-app-react-router -
React Router (not Remix)
-
Shopify Admin embedded app
-
Manual pricing (Billing API)
-
One-time charges (
BillingInterval.OneTime) -
App installed on a dev store
-
App opened from Shopify Admin (not submission preview)
-What I Have Already Confirmed
-
Pricing method is Manual pricing (not Managed pricing)
-
App was uninstalled and reinstalled after pricing change
-
Billing plans are defined only in code, not in Partner Dashboard
-
Billing keys are consistent between UI and
shopify.server.js -
App is accessed via:
https://admin.shopify.com/store/<store>/apps/<app-name> -
I am not using:
-
billing.check() -
billing.require()
-
-
Billing is triggered only via
billing.request()
-Billing Configuration (shopify.server.js)
billing: {
SILVER_ONETIME: {
amount: 50,
currencyCode: "EUR",
interval: BillingInterval.OneTime,
name: "Silver Plan",
},
GOLD_ONETIME: {
amount: 100,
currencyCode: "EUR",
interval: BillingInterval.OneTime,
name: "Gold Plan",
},
PLATINUM_ONETIME: {
amount: 300,
currencyCode: "EUR",
interval: BillingInterval.OneTime,
name: "Platinum Plan",
},
},
- Billing Trigger (UI → Action)
return billing.request({
plan: plan.key,
isTest: true,
returnUrl: `${process.env.SHOPIFY_APP_URL}/app`,
});
-
Triggered via a native HTML
<form method="post"> -
No client-side navigation issues
-
Action executes successfully until billing request
Actual Problem
When clicking “Select Plan”, the Shopify billing approval page does not open.
Instead, the server throws a generic error:
Error: Error while billing the store
at Object.request (.../billing/request.ts)
at Object.requestBilling (.../authenticate/admin/billing/request.ts)
There is:
-
No approval URL
-
No redirect
-
No detailed GraphQL error returned
-
No indication of what exactly is invalid
-Why This Is Confusing
-
All official prerequisites for Billing API appear to be satisfied
-
Manual pricing is enabled
-
One-time billing should be supported
-
The error message is too generic and provides no actionable feedback
-
The same error appears even after reinstalling the app
For the reference I have added the code below as well-
#Below is the code of app. billing.jsx
import { useLoaderData } from “react-router”;
import {
Page,
Card,
Button,
Text,
InlineStack,
BlockStack,
Layout,
Box,
} from “@shopify/polaris”;
import { authenticate } from “../shopify.server”;
const PLANS = {
silver: { name: “Silver”, key: “SILVER_ONETIME”, price: 50 },
gold: { name: “Gold”, key: “GOLD_ONETIME”, price: 100 },
platinum: { name: “Platinum”, key: “PLATINUM_ONETIME”, price: 300 },
};
export const loader = async ({ request }) => {
await authenticate.admin(request);
return { plans: PLANS };
};
export const action = async ({ request }) => {
const { billing } = await authenticate.admin(request);
const formData = await request.formData();
const planId = formData.get(“plan”);
const plan = PLANS[planId];
if (!plan) {
throw new Response("Invalid plan", { status: 400 });
}
return billing.request({
plan: plan.key,
isTest: true,
returnUrl: \`${process.env.SHOPIFY_APP_URL}/app\`,
});
};
export default function Billing() {
const { plans } = useLoaderData();
return (
*<Page* title="Choose your plan"*>*
*<Layout>*
*<Layout.Section>*
*<InlineStack* gap="400" align="center"*>*
{Object.entries(plans).map((\[id, plan\]) => (
*<Card* key={id}*>*
*<Box* padding="500"*>*
*<BlockStack* gap="400" align="center"*>*
*<Text* variant="headingLg"*>*{plan.name}*</Text>*
*<Text* variant="heading2xl"*>*€{plan.price}*</Text>*
*<*form method="post"*>*
*<*input type="hidden" name="plan" value={id} */>*
*<Button* submit variant="primary"*>*
Select Plan
*</Button>*
*</*form*>*
*</BlockStack>*
*</Box>*
*</Card>*
))}
*</InlineStack>*
*</Layout.Section>*
*</Layout>*
*</Page>*
);
}
For the reference I have added the code below as well-
#Below is the code for shopify.server.js
const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY,
apiSecretKey: process.env.SHOPIFY_API_SECRET || “”,
apiVersion: ApiVersion.October25,
scopes: process.env.SCOPES?.split(“,”),
appUrl: process.env.SHOPIFY_APP_URL,
authPathPrefix: “/auth”,
sessionStorage: new PrismaSessionStorage(prisma),
distribution: AppDistribution.AppStore,
…(process.env.SHOP_CUSTOM_DOMAIN
? { customShopDomains: \[process.env.SHOP_CUSTOM_DOMAIN\] }
: {}),
billing: {
SILVER_ONETIME: {
amount: 50,
currencyCode: "EUR",
interval: BillingInterval.OneTime,
name: "Silver Plan",
},
GOLD_ONETIME: {
amount: 100,
currencyCode: "EUR",
interval: BillingInterval.OneTime,
name: "Gold Plan",
},
PLATINUM_ONETIME: {
amount: 300,
currencyCode: "EUR",
interval: BillingInterval.OneTime,
name: "Platinum Plan",
},
},
});
export default shopify;
export const apiVersion = ApiVersion.October25;
export const authenticate = shopify.authenticate;
export const unauthenticated = shopify.unauthenticated;
export const login = shopify.login;
export const registerWebhooks = shopify.registerWebhooks;
export const sessionStorage = shopify.sessionStorage;


