Skip to main content
Selling plans define the subscription options available to customers. This guide shows you how to create, update, and manage selling plans using the reference app.

Understanding Selling Plans

A selling plan group contains:
  • Merchant Code: Internal identifier for the plan
  • Plan Name: Customer-facing name
  • Products: Products/variants included in the plan
  • Delivery Options: Frequency and interval options
  • Discounts: Optional pricing adjustments

Creating a Selling Plan

1

Navigate to the plans page

In the app, go to the Plans page. If you don’t have any plans yet, you’ll see an empty state.
2

Click 'Create Plan'

Click the “Create Plan” button to start creating a new selling plan group.
3

Configure plan details

Fill in the basic plan information:
  • Merchant Code: Unique identifier (e.g., “MONTHLY_COFFEE”)
  • Plan Name: Display name (e.g., “Monthly Coffee Subscription”)
4

Add products

Select the products or variants you want to include in this subscription plan.
5

Configure delivery options

Set up one or more delivery frequencies:
  • Weekly, Monthly, or Yearly
  • Custom interval counts
  • Optional discounts per frequency
6

Save the plan

Click “Save” to create the selling plan group.

Implementation Details

Creating a Selling Plan (Code)

The selling plan creation flow is handled in app/routes/app.plans.$id/route.tsx:
import { createSellingPlanGroup } from '~/models/SellingPlan/SellingPlan.server';

export async function action({ request, params }: ActionFunctionArgs) {
  const { admin } = await authenticate.admin(request);
  const planID = params.id;

  // Validate form data
  const validationResult = await validateFormData(
    getSellingPlanFormSchema(t),
    await request.formData()
  );

  if (validationResult.error) {
    return validationError(validationResult.error);
  }

  const {
    merchantCode,
    planName,
    selectedProductIds,
    selectedVariantIds,
    discountDeliveryOptions,
    offerDiscount,
    discountType,
    shopCurrencyCode,
  } = validationResult.data;

  if (planID === 'create') {
    const { sellingPlanGroupId, userErrors } = await createSellingPlanGroup(
      admin.graphql,
      {
        merchantCode,
        name: planName,
        productIds: formStringToArray(selectedProductIds),
        productVariantIds: formStringToArray(selectedVariantIds),
        discountDeliveryOptions: discountDeliveryOptions || [],
        offerDiscount: Boolean(offerDiscount),
        discountType: discountType || DiscountType.PERCENTAGE,
        currencyCode: shopCurrencyCode || 'USD',
      }
    );

    if (!sellingPlanGroupId || userErrors?.length) {
      return json(
        toast(userErrors?.[0]?.message || t('SubscriptionPlanForm.createError'), 
        { isError: true }),
        { status: 500 }
      );
    }

    return redirect(`/app/plans/${parseGid(sellingPlanGroupId)}?planCreated=true`);
  }
}

Server-Side Model

The createSellingPlanGroup function in app/models/SellingPlan/SellingPlan.server.ts:
interface CreateSellingPlanGroupInput {
  name: string;
  merchantCode: string;
  productIds: string[];
  productVariantIds: string[];
  discountDeliveryOptions: DiscountDeliveryOption[];
  discountType: DiscountTypeType;
  offerDiscount: boolean;
  currencyCode: string;
}

export async function createSellingPlanGroup(
  graphql,
  input: CreateSellingPlanGroupInput
) {
  const {
    name,
    merchantCode,
    productIds,
    productVariantIds,
    discountDeliveryOptions,
    discountType,
    offerDiscount,
    currencyCode,
  } = input;

  const { primaryLocale } = await getShopLocales(graphql);
  const t = await i18n.getFixedT(primaryLocale, 'app.plans.details');

  const sellingPlansToCreate = getSellingPlansFromDiscountDeliveryOptions(
    discountDeliveryOptions,
    discountType,
    offerDiscount,
    currencyCode,
    t,
    primaryLocale
  );

  const response = await graphql(CreateSellingPlanGroupMutation, {
    variables: {
      input: {
        name,
        merchantCode,
        options: [t('creationMutation.options.deliveryFrequency')],
        sellingPlansToCreate,
      },
      resources: {
        productIds,
        productVariantIds,
      },
    },
  });

  const {
    data: { sellingPlanGroupCreate },
  } = await response.json();

  return {
    sellingPlanGroupId: sellingPlanGroupCreate?.sellingPlanGroup?.id,
    userErrors: sellingPlanGroupCreate?.userErrors,
  };
}

Discount and Delivery Options

Configuring Discounts

You can offer two types of discounts:

Percentage Discount

const discountDeliveryOption = {
  id: 'new-1',
  deliveryInterval: 'MONTH',
  deliveryFrequency: 1,
  discountValue: 10, // 10% off
};

Fixed Amount Discount

const discountDeliveryOption = {
  id: 'new-2',
  deliveryInterval: 'MONTH',
  deliveryFrequency: 1,
  discountValue: 5.00, // $5 off
};
The discount type (percentage vs. fixed amount) is set at the selling plan group level and applies to all delivery options within that group.

Multiple Delivery Frequencies

You can create multiple delivery options in a single selling plan:
const discountDeliveryOptions = [
  {
    id: 'new-1',
    deliveryInterval: 'WEEK',
    deliveryFrequency: 1,
    discountValue: 5,
  },
  {
    id: 'new-2',
    deliveryInterval: 'MONTH',
    deliveryFrequency: 1,
    discountValue: 10,
  },
  {
    id: 'new-3',
    deliveryInterval: 'MONTH',
    deliveryFrequency: 3,
    discountValue: 15,
  },
];
This creates three subscription options:
  • Weekly delivery with 5% discount
  • Monthly delivery with 10% discount
  • Quarterly delivery with 15% discount

Updating a Selling Plan

To update an existing selling plan:
export async function updateSellingPlanGroup(
  graphql,
  input: UpdateSellingPlanGroupVariables
) {
  const { primaryLocale } = await getShopLocales(graphql);
  const t = await i18n.getFixedT(primaryLocale, 'app.plans.details');

  const sellingPlans = getSellingPlansFromDiscountDeliveryOptions(
    input.discountDeliveryOptions,
    input.discountType,
    input.offerDiscount,
    input.currencyCode,
    t,
    primaryLocale
  );

  let sellingPlansToCreate = [];
  let sellingPlansToUpdate = [];

  // Separate new plans from existing ones
  sellingPlans.forEach((sellingPlan) => {
    if (sellingPlan.id) {
      sellingPlansToUpdate.push(sellingPlan);
    } else {
      sellingPlansToCreate.push(sellingPlan);
    }
  });

  const variables = {
    id: input.id,
    input: {
      name: input.name,
      merchantCode: input.merchantCode,
      sellingPlansToDelete: input.sellingPlansToDelete,
      sellingPlansToCreate,
      sellingPlansToUpdate,
    },
    productIdsToAdd: input.productIdsToAdd,
    productIdsToRemove: input.productIdsToRemove,
    productVariantIdsToAdd: input.productVariantIdsToAdd,
    productVariantIdsToRemove: input.productVariantIdsToRemove,
  };

  const response = await graphql(SellingPlanGroupUpdateMutation, {
    variables,
  });

  return {
    sellingPlanGroupId: response.data.sellingPlanGroupUpdate?.sellingPlanGroup?.id,
    userErrors: [...(response.data.sellingPlanGroupUpdate?.userErrors ?? [])],
  };
}

Deleting a Selling Plan

To delete a selling plan group:
import { deleteSellingPlanGroup } from '~/models/SellingPlan/SellingPlan.server';

export async function action({ request }: ActionFunctionArgs) {
  const { admin } = await authenticate.admin(request);
  const body = await request.formData();
  const sellingPlanGroupIds: string[] = String(body.get('ids'))?.split(',');

  const response = await Promise.allSettled(
    sellingPlanGroupIds.map(async (id: string) => {
      await deleteSellingPlanGroup(admin.graphql, id);
    })
  );

  if (response.some(isRejected)) {
    return json(
      toast(t('table.deletePlan.toast.failed'), { isError: true }),
      { status: 500 }
    );
  }

  return json(
    toast(t('table.deletePlan.toast.success', { count: sellingPlanGroupIds.length }))
  );
}
Deleting a selling plan group will affect existing subscriptions using that plan. Ensure you understand the impact before deletion.

Listing Selling Plans

To retrieve and display all selling plans:
import { getSellingPlanGroups } from '~/models/SellingPlan/SellingPlan.server';

export async function loader({ request }) {
  const { admin } = await authenticate.admin(request);
  const url = new URL(request.url);

  const { sellingPlanGroups, pageInfo } = await getSellingPlanGroups(
    admin.graphql,
    {
      first: 50,
      // Add pagination cursors from URL params
      after: url.searchParams.get('after'),
      before: url.searchParams.get('before'),
    }
  );

  return {
    sellingPlanGroups,
    sellingPlanGroupPageInfo: pageInfo,
  };
}

Best Practices

Plan Organization
  • Use descriptive merchant codes (e.g., “COFFEE_MONTHLY” instead of “PLAN1”)
  • Group related products together
  • Offer multiple frequency options to match customer preferences
Discount Strategy
  • Offer higher discounts for longer commitment periods
  • Keep discount percentages consistent within a product category
  • Test different discount levels to optimize conversion
Product Selection
  • Only include products suitable for recurring delivery
  • Consider creating separate plans for different product categories
  • Use variants when offering size or flavor options

GraphQL Mutations

The app uses these GraphQL mutations for selling plan management:
  • CreateSellingPlanGroupMutation - Creates a new selling plan group
  • SellingPlanGroupUpdateMutation - Updates an existing group
  • DeleteSellingPlanGroupMutation - Deletes a group
  • SellingPlanGroupQuery - Fetches details for a single group
  • SellingPlanGroupsQuery - Lists all selling plan groups

Next Steps

Managing Subscriptions

Learn how to manage active subscription contracts

Handling Webhooks

Process subscription lifecycle events

Build docs developers (and LLMs) love