---
title: Redirect to the plan selection page
description: >-
  Learn how to redirect merchants to your plan selection page after they install
  your React Router app.
source_url:
  html: >-
    https://shopify.dev/docs/apps/launch/billing/shopify-app-pricing/redirect-plan-selection-page
  md: >-
    https://shopify.dev/docs/apps/launch/billing/shopify-app-pricing/redirect-plan-selection-page.md
---

# Redirect to the plan selection page

A common pattern is to redirect merchants to your [plan selection page](https://shopify.dev/docs/apps/launch/billing/shopify-app-pricing#plan-selection-page) after they install your app. However, because [embedded apps](https://shopify.dev/docs/apps/build/admin) are rendered in the Shopify admin inside an iframe, they don't have permission to manipulate the parent browser window, including redirects.

[Shopify's React Router package](https://shopify.dev/docs/api/shopify-app-react-router) provides utilities that enable apps to redirect elsewhere in the Shopify admin. This keeps the user experience smooth while working within iframe constraints.

***

## Requirements

* An embedded app [scaffolded with React Router](https://shopify.dev/docs/apps/build/scaffold-app). This includes the [@shopify/shopify-app-react-router](https://shopify.dev/docs/api/shopify-app-react-router) package by default.
* Your app needs to have [Shopify App Pricing enabled](https://shopify.dev/docs/apps/launch/billing/shopify-app-pricing#opt-in-to-shopify-app-pricing), with at least one plan configured.
* A [Partner API client](https://shopify.dev/docs/api/partner#create-a-partner-api-client) with the **Manage apps** permission. The [Active Subscription API](https://shopify.dev/docs/api/partner/active-subscription) requires this permission, and it supports only public apps.

***

## Check subscription status in React Router

The example in this section runs in the [loader](https://reactrouter.com/start/framework/data-loading) for the app's root route, so it works no matter which app route the user arrives at. It does the following:

1. Queries the [Active Subscription API](https://shopify.dev/docs/api/partner/active-subscription) on the Partner API to check whether the shop has an active Shopify App Pricing subscription. With Shopify App Pricing, subscription status comes from the Partner API, not from the GraphQL Admin API.
2. If not, then redirects to the plan selection page with the [redirect utility](https://shopify.dev/docs/api/shopify-app-react-router/authenticate/admin#example-redirect) from `@shopify/shopify-app-react-router`, which can navigate outside the app frame.
3. If there is an active subscription, then renders your app's content normally.

The example reads the following values:

* `YOUR_APP_HANDLE`: The `handle` from your `shopify.app.toml` file. The store handle comes from the session's shop domain (for example, `cool-shop` from `cool-shop.myshopify.com`).
* `SHOPIFY_PARTNER_ORG_ID`: Your organization ID, which appears in your Partner Dashboard URL.
* `SHOPIFY_PARTNER_API_ACCESS_TOKEN`: The access token for your Partner API client. Refer to [Partner API authentication](https://shopify.dev/docs/api/partner#authentication).
* `SHOPIFY_APP_GID`: Your app's GID in the form `gid://shopify/App/{app_id}`, where `{app_id}` is the numeric ID in your app's Partner Dashboard URL.

`activeSubscription` requires the shop's GID rather than its `myshopify.com` domain, so the example queries `shop { id }` through the GraphQL Admin API. The Partner API has a [rate limit](https://shopify.dev/docs/api/partner#rate-limits) of four requests per second for each client, so the example throws on throttled or failed responses instead of treating them as a missing subscription. In production, cache only a confirmed subscription for each shop, with a short expiry such as five minutes. A merchant who just approved a plan is then checked immediately, and a cancellation or freeze is picked up when the cached entry expires.

The route file handles the check and redirect. The Partner API request lives in its own server module so that other routes can reuse it.

## app/routes/app.jsx

```javascript
import { fetchActiveSubscription } from "../partner-api.server";


export const loader = async ({ request }) => {
  // Replace with the "handle" from your shopify.app.toml file
  const appHandle = "YOUR_APP_HANDLE";


  // Authenticate with Shopify credentials to handle server-side queries
  const { authenticate } = await import("../shopify.server");


  // Get the GraphQL Admin API client and redirect utility
  const { admin, redirect, session } = await authenticate.admin(request);


  // Extract the store handle from the shop domain
  // for example, "cool-shop" from "cool-shop.myshopify.com"
  const storeHandle = session.shop.replace(".myshopify.com", "");


  // Get the shop GID that the Partner API requires
  const shopResponse = await admin.graphql(`{ shop { id } }`);
  const { data: { shop: { id: shopId } } } = await shopResponse.json();


  // Check whether the store has an active Shopify App Pricing subscription
  const subscription = await fetchActiveSubscription(shopId);


  // If there's no active subscription, redirect to the plan selection page...
  if (!subscription) {
    return redirect(`https://admin.shopify.com/store/${storeHandle}/charges/${appHandle}/pricing_plans`, {
      target: "_top", // required because the URL is outside the app scope
    });
  }


  // ...Otherwise, continue loading the app as normal
  return {
    apiKey: process.env.SHOPIFY_API_KEY || "",
  };
};
```

## app/partner-api.server.js

```javascript
export async function fetchActiveSubscription(shopId) {
  const res = await fetch(
    `https://partners.shopify.com/${process.env.SHOPIFY_PARTNER_ORG_ID}/api/2026-07/graphql.json`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Shopify-Access-Token": process.env.SHOPIFY_PARTNER_API_ACCESS_TOKEN,
      },
      body: JSON.stringify({
        query: `query ($appId: ID!, $shopId: ID!) {
          activeSubscription(appId: $appId, shopId: $shopId) { billingPeriod }
        }`,
        variables: {
          appId: process.env.SHOPIFY_APP_GID,
          shopId,
        },
      }),
    }
  );
  const { data, errors } = await res.json();


  // Throw on throttling or other failures so the loader doesn't redirect a paying user
  if (!res.ok || errors) {
    throw new Error(`Partner API request failed: ${JSON.stringify(errors ?? res.status)}`);
  }


  // activeSubscription is null only when the shop has no active subscription for the app
  return data.activeSubscription;
}
```

**Info:**

`activeSubscription` returns both Billing API and Shopify App Pricing subscriptions, so subscriptions that you haven't [migrated](https://shopify.dev/docs/apps/launch/billing/shopify-app-pricing/migrating-to-shopify-app-pricing#for-apps-on-manual-pricing-billing-api) yet don't need a separate check. One-time purchases aren't subscriptions. If existing one-time purchases grant access, then also check `billing.check()` before redirecting. It queries `currentAppInstallation` and returns `hasActivePayment: true` for active one-time purchases. Remove that call after no existing one-time purchases grant access.

***

## Next steps

* Learn more about [Shopify App Pricing](https://shopify.dev/docs/apps/launch/billing/shopify-app-pricing).
* Query subscription data with the [Active Subscription API](https://shopify.dev/docs/api/partner/active-subscription).

***
