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
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.
Click 'Create Plan'
Click the “Create Plan” button to start creating a new selling plan group.
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”)
Add products
Select the products or variants you want to include in this subscription plan.
Configure delivery options
Set up one or more delivery frequencies:
Weekly, Monthly, or Yearly
Custom interval counts
Optional discounts per frequency
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