This guide covers all subscription contract management operations in the Shopify Subscriptions Reference App, from viewing contract details to performing lifecycle operations.
Viewing Subscription Contracts
List All Contracts
The main contracts page displays all subscription contracts with filtering and pagination:
import { getContracts } from '~/models/SubscriptionContract/SubscriptionContract.server' ;
export async function loader ({ request }) {
const { admin } = await authenticate . admin ( request );
const url = new URL ( request . url );
const { subscriptionContracts , subscriptionContractPageInfo } = await getContracts (
admin . graphql ,
{
first: 50 ,
after: url . searchParams . get ( 'after' ),
before: url . searchParams . get ( 'before' ),
}
);
return {
subscriptionContracts ,
subscriptionContractPageInfo ,
};
}
View Contract Details
To view a single contract with full details:
import { getContractDetails } from '~/models/SubscriptionContract/SubscriptionContract.server' ;
export async function loader ({ params , request }) {
const { admin } = await authenticate . admin ( request );
const id = params . id ;
const gid = composeGid ( 'SubscriptionContract' , id );
const subscriptionContract = await getContractDetails ( admin . graphql , gid );
return {
subscriptionContract ,
upcomingBillingCycles ,
pastBillingCycles ,
};
}
This returns comprehensive contract information including:
Customer details
Line items
Billing and delivery policies
Payment method
Upcoming and past billing cycles
Pausing Subscriptions
Navigate to contract details
Go to the subscription contract you want to pause.
Click the Pause action
From the contract actions menu, select “Pause”.
Confirm the action
The contract status will update to “Paused” and no further billing will occur.
Pause Implementation
The pause action is implemented using a custom hook in app/routes/app.contracts.$id._index/hooks/usePauseAction.ts:
import { useSubmit , useNavigation } from '@remix-run/react' ;
export function usePauseAction () {
const submit = useSubmit ();
const navigation = useNavigation ();
const pauseContract = () => {
submit (
{ action: 'pause' },
{ method: 'post' , action: './pause' }
);
};
const pauseLoading = navigation . state === 'submitting' &&
navigation . formAction ?. includes ( 'pause' );
return { pauseContract , pauseLoading };
}
Server-Side Pause Handler
The route handler processes the pause request:
export async function action ({ request , params } : ActionFunctionArgs ) {
const { admin } = await authenticate . admin ( request );
const contractId = composeGid ( 'SubscriptionContract' , params . id );
const response = await admin . graphql (
`mutation subscriptionContractPause($contractId: ID!) {
subscriptionContractPause(subscriptionContractId: $contractId) {
contract {
id
status
}
userErrors {
field
message
}
}
}` ,
{
variables: { contractId },
}
);
const { data } = await response . json ();
if ( data . subscriptionContractPause . userErrors ?. length ) {
return json (
toast ( t ( 'actions.pause.error' ), { isError: true }),
{ status: 500 }
);
}
return json ( toast ( t ( 'actions.pause.success' )));
}
Resuming Subscriptions
Navigate to paused contract
Go to a contract with “Paused” status.
Click Resume
Select “Resume” from the actions menu.
Billing resumes
The contract status updates to “Active” and billing resumes on the next cycle.
Resume Implementation
export function useResumeAction () {
const submit = useSubmit ();
const navigation = useNavigation ();
const resumeContract = () => {
submit (
{ action: 'resume' },
{ method: 'post' , action: './resume' }
);
};
const resumeLoading = navigation . state === 'submitting' &&
navigation . formAction ?. includes ( 'resume' );
return { resumeContract , resumeLoading };
}
Server-Side Resume Handler
export async function action ({ request , params } : ActionFunctionArgs ) {
const { admin } = await authenticate . admin ( request );
const contractId = composeGid ( 'SubscriptionContract' , params . id );
const response = await admin . graphql (
`mutation subscriptionContractResume($contractId: ID!) {
subscriptionContractActivate(subscriptionContractId: $contractId) {
contract {
id
status
}
userErrors {
field
message
}
}
}` ,
{
variables: { contractId },
}
);
const { data } = await response . json ();
if ( data . subscriptionContractActivate . userErrors ?. length ) {
return json (
toast ( t ( 'actions.resume.error' ), { isError: true }),
{ status: 500 }
);
}
return json ( toast ( t ( 'actions.resume.success' )));
}
Canceling Subscriptions
Open cancel modal
From the contract page, click “Cancel” in the actions menu.
Confirm cancellation
A modal appears asking for confirmation.
Submit cancellation
Click “Cancel subscription” to confirm.
Contract cancelled
The contract status updates to “Cancelled” and no further billing occurs.
Cancel Modal Component
From app/routes/app.contracts.$id._index/components/CancelSubscriptionModal/CancelSubscriptionModal.tsx:
import { Modal , Text } from '@shopify/polaris' ;
import { useSubmit , useNavigation } from '@remix-run/react' ;
interface CancelSubscriptionModalProps {
open : boolean ;
onClose : () => void ;
}
export function CancelSubscriptionModal ({
open ,
onClose
} : CancelSubscriptionModalProps ) {
const submit = useSubmit ();
const navigation = useNavigation ();
const { t } = useTranslation ( 'app.contracts' );
const loading = navigation . state === 'submitting' ;
const handleCancel = () => {
submit (
{ action: 'cancel' },
{ method: 'post' , action: './cancel' }
);
};
return (
< Modal
open = { open }
onClose = { onClose }
title = { t ( 'actions.cancel.modal.title' )}
primaryAction = {{
content : t ( 'actions.cancel.modal.confirm' ),
onAction : handleCancel ,
loading ,
destructive : true ,
}}
secondaryActions = { [
{
content: t ( 'actions.cancel.modal.cancel' ),
onAction: onClose,
},
]}
>
<Modal.Section>
<Text as="p">
{t('actions.cancel.modal.description')}
</Text>
</Modal.Section>
</Modal>
);
}
Server-Side Cancel Handler
export async function action ({ request , params } : ActionFunctionArgs ) {
const { admin } = await authenticate . admin ( request );
const contractId = composeGid ( 'SubscriptionContract' , params . id );
const response = await admin . graphql (
`mutation subscriptionContractCancel($contractId: ID!) {
subscriptionContractCancel(subscriptionContractId: $contractId) {
contract {
id
status
}
userErrors {
field
message
}
}
}` ,
{
variables: { contractId },
}
);
const { data } = await response . json ();
if ( data . subscriptionContractCancel . userErrors ?. length ) {
return json (
toast ( t ( 'actions.cancel.error' ), { isError: true }),
{ status: 500 }
);
}
return redirect ( `/app/contracts/ ${ params . id } ` );
}
Cancellation is permanent and cannot be reversed. Consider offering a pause option as an alternative.
Updating Contract Details
Subscription contracts can be edited to modify:
Line items (add, remove, update quantities)
Delivery frequency
Product prices
Billing and delivery policies
Edit Flow Implementation
From app/routes/app.contracts.$id.edit/route.tsx:
export async function action ({ request , params } : ActionFunctionArgs ) {
const { admin , session } = await authenticate . admin ( request );
const shopDomain = session . shop ;
const contractId = params . id ;
const gid = composeGid ( 'SubscriptionContract' , contractId );
// Validate form data
const validationResult = await validateFormData (
getContractEditFormSchema ( t ),
await request . formData ()
);
if ( validationResult . error ) {
return json (
toast ( t ( 'edit.actions.updateContract.error' ), { isError: true })
);
}
const { lines , deliveryPolicy } = validationResult . data ;
// Get current contract state
const { lines : initialLines , deliveryPolicy : initialDeliveryPolicy } =
await getContractEditDetails ( admin . graphql , gid );
// Determine changes
const linesToAdd = getLinesToAdd ( initialLines , lines );
const lineIdsToRemove = getLineIdsToRemove ( initialLines , lines );
const linesToUpdate = getLinesToUpdate ( initialLines , lines );
const deliveryPolicyChanged =
deliveryPolicy . interval !== initialDeliveryPolicy . interval ||
deliveryPolicy . intervalCount !== initialDeliveryPolicy . intervalCount ;
// Create a draft to apply changes
const draft = await buildDraftFromContract ( shopDomain , gid , admin . graphql );
// Add new lines
if ( linesToAdd . length > 0 ) {
await Promise . all (
linesToAdd . map ( async ( line ) => {
await draft . addLine ( line );
})
);
}
// Remove lines
if ( lineIdsToRemove . length > 0 ) {
await Promise . all (
lineIdsToRemove . map ( async ( lineId ) => {
await draft . removeLine ( lineId );
})
);
}
// Update existing lines
if ( linesToUpdate . length > 0 ) {
await Promise . all (
linesToUpdate . map ( async ( line ) => {
const lineUpdateInput = {
quantity: line . quantity ,
currentPrice: line . price ,
... ( line . pricingPolicy && { pricingPolicy: line . pricingPolicy }),
};
await draft . updateLine ( line . id , lineUpdateInput );
})
);
}
// Update delivery policy if changed
if ( deliveryPolicyChanged ) {
await draft . update ({
billingPolicy: deliveryPolicy ,
deliveryPolicy ,
});
}
// Commit all changes
const committed = await draft . commit ();
if ( ! committed ) {
return json (
toast ( t ( 'edit.actions.updateContract.error' ), { isError: true })
);
}
return json ( toast ( t ( 'edit.actions.updateContract.success' )));
}
Updating Customer Address
Customers can update their shipping address:
export async function action ({ request , params } : ActionFunctionArgs ) {
const { admin } = await authenticate . admin ( request );
const formData = await request . formData ();
const contractId = composeGid ( 'SubscriptionContract' , params . id );
const addressId = formData . get ( 'addressId' );
const response = await admin . graphql (
`mutation subscriptionContractSetNextBillingDate(
$contractId: ID!
$shippingAddress: MailingAddressInput!
) {
subscriptionDraftUpdate(
draftId: $contractId
input: { deliveryMethod: { shipping: { address: $shippingAddress } } }
) {
draft {
id
}
userErrors {
field
message
}
}
}` ,
{
variables: {
contractId ,
shippingAddress: formatAddressInput ( formData ),
},
}
);
const { data } = await response . json ();
if ( data . subscriptionDraftUpdate . userErrors ?. length ) {
return json (
toast ( t ( 'customer.address.error' ), { isError: true }),
{ status: 500 }
);
}
return json ( toast ( t ( 'customer.address.success' )));
}
Updating Payment Method
Customers can request a payment method update email:
import { CustomerPaymentMethodSendUpdateEmailMutation } from '~/graphql/CustomerPaymentMethodSendUpdateEmailMutation' ;
export async function action ({ request , params } : ActionFunctionArgs ) {
const { admin } = await authenticate . admin ( request );
const formData = await request . formData ();
const paymentMethodId = formData . get ( 'paymentMethodId' ) as string ;
const response = await admin . graphql (
CustomerPaymentMethodSendUpdateEmailMutation ,
{
variables: {
customerPaymentMethodId: paymentMethodId ,
},
}
);
const { data } = await response . json ();
if ( data . customerPaymentMethodSendUpdateEmail . userErrors ?. length ) {
return json (
toast ( t ( 'payment.updateEmail.error' ), { isError: true }),
{ status: 500 }
);
}
return json ( toast ( t ( 'payment.updateEmail.success' )));
}
Contract Statuses
Subscription contracts have the following statuses:
Status Description Available Actions Active Contract is active and billing Pause, Cancel, Edit Paused Temporarily paused by merchant/customer Resume, Cancel, Edit Cancelled Permanently cancelled None Failed Billing attempt failed Pause, Cancel, Retry Billing Expired Contract has reached its end date None
Best Practices
Communication
Always notify customers before making changes to their subscriptions
Send confirmation emails after pause, resume, or cancel actions
Provide clear explanations for any billing failures
Draft Pattern
Use the draft pattern for complex updates to ensure atomicity
Validate all changes before committing the draft
Handle errors gracefully and provide clear feedback
Error Handling
Always check for userErrors in GraphQL responses
Provide helpful error messages to users
Log errors for debugging and monitoring
Next Steps
Handling Webhooks Process subscription lifecycle events
Testing Test subscription flows and edge cases