Subscription renewal emails directly impact retention and revenue. Well-timed, clear communications reduce churn and failed payments. Here's how to design an effective renewal email flow.
Renewal email sequence
Timeline
const renewalSequence = [
{ daysBefore: 14, template: 'renewal-reminder-14d' },
{ daysBefore: 7, template: 'renewal-reminder-7d' },
{ daysBefore: 3, template: 'renewal-reminder-3d' },
{ daysBefore: 0, template: 'renewal-processed' },
{ daysAfter: 1, template: 'renewal-failed', condition: 'payment_failed' }
];
Reminder email
interface RenewalReminder {
user: User;
subscription: {
plan: string;
amount: number;
currency: string;
renewsAt: Date;
paymentMethod: string;
};
}
await sendEmail({
to: user.email,
subject: `Your ${subscription.plan} plan renews in ${daysUntil} days`,
template: 'renewal-reminder',
data: {
user,
subscription,
updatePaymentUrl: `${baseUrl}/billing/payment-method`,
changePlanUrl: `${baseUrl}/billing/plans`,
cancelUrl: `${baseUrl}/billing/cancel`
}
});
Renewal confirmation
await sendEmail({
to: user.email,
subject: `Your ${subscription.plan} plan has renewed`,
template: 'renewal-confirmation',
data: {
user,
subscription,
invoice: {
id: invoice.id,
amount: invoice.amount,
pdfUrl: invoice.pdfUrl
},
nextRenewal: subscription.nextRenewalDate,
usageSummary: await getUsageSummary(user.id)
}
});
Failed payment recovery
Dunning sequence
const dunningSequence = [
{
attempt: 1,
delay: 0,
template: 'payment-failed-1',
subject: 'Payment failed - please update your card'
},
{
attempt: 2,
delay: 3, // days
template: 'payment-failed-2',
subject: 'Second attempt failed - action required'
},
{
attempt: 3,
delay: 7,
template: 'payment-failed-3',
subject: 'Final notice - your account will be suspended'
},
{
attempt: 4,
delay: 10,
template: 'account-suspended',
subject: 'Your account has been suspended'
}
];
Payment failed email
await sendEmail({
to: user.email,
subject: 'Payment failed - please update your card',
template: 'payment-failed',
data: {
user,
subscription,
failureReason: getReadableFailureReason(payment.failureCode),
retryDate: payment.nextRetryAt,
updateCardUrl: `${baseUrl}/billing/payment-method`,
gracePeriodEnds: subscription.gracePeriodEnds
},
priority: 'high'
});
function getReadableFailureReason(code: string): string {
const reasons = {
'card_declined': 'Your card was declined',
'insufficient_funds': 'Insufficient funds',
'expired_card': 'Your card has expired',
'incorrect_cvc': 'Incorrect security code',
'processing_error': 'Processing error - we\'ll retry'
};
return reasons[code] || 'Payment could not be processed';
}
Plan change notifications
Upgrade confirmation
await sendEmail({
to: user.email,
subject: `Welcome to ${newPlan.name}!`,
template: 'plan-upgraded',
data: {
user,
previousPlan: oldPlan.name,
newPlan: {
name: newPlan.name,
features: getNewFeatures(oldPlan, newPlan),
price: newPlan.price
},
prorated: {
credit: proratedCredit,
charged: proratedCharge
},
gettingStartedUrl: `${baseUrl}/getting-started/${newPlan.slug}`
}
});
Downgrade confirmation
await sendEmail({
to: user.email,
subject: `Your plan change to ${newPlan.name}`,
template: 'plan-downgraded',
data: {
user,
currentPlan: oldPlan.name,
newPlan: newPlan.name,
effectiveDate: downgrade.effectiveDate,
lostFeatures: getLostFeatures(oldPlan, newPlan),
dataRetention: 'Your data will be preserved',
upgradeUrl: `${baseUrl}/billing/plans`
}
});
Cancellation flow
Cancellation confirmation
await sendEmail({
to: user.email,
subject: 'Your subscription has been cancelled',
template: 'subscription-cancelled',
data: {
user,
subscription: {
plan: subscription.plan,
endsAt: subscription.currentPeriodEnd,
// Access continues until period end
accessUntil: subscription.currentPeriodEnd
},
dataExport: {
available: true,
exportUrl: `${baseUrl}/settings/export`
},
feedback: {
url: `${baseUrl}/feedback/cancellation`,
incentive: 'Help us improve and get 1 month free if you return'
},
reactivateUrl: `${baseUrl}/billing/reactivate`
}
});
Win-back after cancellation
// Send 7 days before access ends
await sendEmail({
to: user.email,
subject: 'Your access ends in 7 days',
template: 'access-ending-soon',
data: {
user,
endsAt: subscription.currentPeriodEnd,
whatYouLose: [
`${usage.projectCount} projects`,
`${usage.teamMembers} team members`,
'All integrations'
],
specialOffer: {
discount: '30% off for 3 months',
code: 'COMEBACK30',
expiresAt: subscription.currentPeriodEnd
},
reactivateUrl: `${baseUrl}/billing/reactivate?offer=COMEBACK30`
}
});
Annual renewal handling
Annual renewal reminder
// 30 days before annual renewal
await sendEmail({
to: user.email,
subject: 'Your annual subscription renews soon',
template: 'annual-renewal-reminder',
data: {
user,
subscription: {
plan: subscription.plan,
amount: subscription.annualPrice,
renewsAt: subscription.renewsAt,
savings: calculateAnnualSavings(subscription)
},
yearInReview: {
// Show value received
emailsSent: usage.totalEmails,
uptime: '99.99%',
supportTickets: usage.supportTickets
},
options: {
continueAnnual: 'Continue annual billing',
switchMonthly: 'Switch to monthly',
cancel: 'Cancel subscription'
},
manageUrl: `${baseUrl}/billing`
}
});
Best practices
- —Remind early - Give users time to update payment methods
- —Be specific - Include exact amounts and dates
- —Explain failures - Tell users why payment failed
- —Provide easy fixes - One-click to update payment
- —Show value - Remind users what they're paying for
- —Grace periods - Don't cut access immediately
- —Multiple channels - Consider SMS for failed payments
Renewal emails are about reducing friction and preventing involuntary churn. Make it easy for users to stay subscribed.