How to Build a Shopify Subscription Management System
Reduce subscriber churn with automated dunning, win-back flows, and proactive retention. A technical guide to subscription automation for Shopify stores.
Picture this: you are running a DTC brand with 4,000 active subscribers generating $120K in monthly recurring revenue. On paper, things look great. But when you pull the numbers, you find that 180 subscribers churned last month. About 60 of those were voluntary -- they decided to cancel. The other 120? Failed payments. Credit cards expired, billing addresses changed, insufficient funds. Those 120 subscribers never made a conscious decision to leave. They just... drifted away because nobody followed up.
At an average order value of $45, that is $5,400 in monthly revenue lost to payment failures alone. Over a year, it compounds to nearly $65,000 -- and that is before accounting for the lifetime value of those lost subscribers. For most subscription businesses, involuntary churn represents 40-60% of total churn. It is the single biggest lever you can pull to grow recurring revenue, and most stores barely address it.
This guide walks through how to build a subscription management system that actively fights churn -- from dunning automation that recovers failed payments, to pre-cancellation flows that save subscribers who are about to leave, to a churn prediction pipeline that identifies at-risk customers before they ever hit the cancel button.
Why Subscription Churn Compounds So Aggressively
Before diving into the technical build, it is worth understanding the math behind subscription churn -- because the numbers are worse than most operators realize.
A 5% monthly churn rate sounds manageable. But compounded over 12 months, you are losing 46% of your subscriber base annually. To maintain the same revenue, you need to acquire nearly as many new subscribers as you already have. Every month, you are running harder just to stay in place.
The real damage is in lifetime value. A subscriber paying $45/month who stays for 12 months is worth $540. If they churn at month 3 because of a failed payment you never retried, you captured $135 instead of $540. That is $405 in unrealized revenue from a single subscriber who never intended to cancel.
Here is how churn typically breaks down for a Shopify subscription business:
| Churn Type | % of Total Churn | Root Cause | Recovery Potential |
|---|---|---|---|
| Involuntary (payment failure) | 40-60% | Expired cards, insufficient funds, bank declines | 30-70% recoverable with proper dunning |
| Passive (no action) | 15-25% | Forgot about subscription, lost interest gradually | 20-40% saveable with engagement flows |
| Active (intentional cancel) | 20-35% | Price, product fit, switching to competitor | 10-25% saveable with retention offers |
The first two categories -- involuntary and passive churn -- are almost entirely addressable through automation. You do not need to change your product, lower your prices, or hire a retention team. You need systems that follow up at the right time with the right message.
The Subscription Platform Foundation
Most Shopify subscription businesses run on one of three platforms: Recharge, Loop (formerly Bold Subscriptions), or Shopify's native Subscriptions API. Your choice of platform determines your automation surface area.
Recharge is the most common and has the most mature API. It supports webhooks for every meaningful event -- subscription created, charge upcoming, charge failed, subscription cancelled -- and exposes endpoints for modifying subscriptions programmatically (skip, swap, pause, change frequency).
Loop offers similar webhook coverage with a focus on the subscriber portal experience. Its API is newer but well-documented.
Shopify Subscriptions API is the native option, which avoids third-party dependencies but offers fewer built-in retention features. You will need to build more from scratch.
For this guide, the examples use Recharge's API, but the architecture applies to any subscription platform with webhook support and programmatic subscription management.
Building Dunning Management Automation
Dunning -- the process of recovering failed payments -- is where you will get the highest ROI from automation. A well-built dunning sequence recovers 30-70% of failed charges, depending on your retry timing and communication strategy.
Step 1: Listen for Failed Charges
Set up a webhook listener for Recharge's charge/failed event. When a payment fails, Recharge sends a payload containing the charge details, the failure reason, and the subscriber's information:
{
"charge": {
"id": 987654321,
"customer_id": 12345678,
"status": "error",
"error_type": "CARD_DECLINED",
"error": "Your card was declined.",
"scheduled_at": "2026-03-23T00:00:00",
"total_price": "45.00",
"retry_date": "2026-03-26T00:00:00",
"analytics_data": {
"utm_params": []
}
}
}
In n8n, create a Webhook node to receive this event, then immediately classify the failure type. Different failure reasons require different strategies:
// Classify payment failure and determine dunning strategy
const charge = $input.item.json.charge;
const errorType = charge.error_type;
let strategy;
switch (errorType) {
case 'CARD_EXPIRED':
strategy = {
type: 'card_update',
urgency: 'high',
retryDelay: 0, // No point retrying same card
message: 'update_card',
maxRetries: 0
};
break;
case 'INSUFFICIENT_FUNDS':
strategy = {
type: 'retry_later',
urgency: 'medium',
retryDelay: 3, // Retry in 3 days
message: 'payment_failed_soft',
maxRetries: 3
};
break;
case 'CARD_DECLINED':
strategy = {
type: 'retry_escalate',
urgency: 'medium',
retryDelay: 2, // Retry in 2 days
message: 'payment_failed_generic',
maxRetries: 4
};
break;
case 'FRAUD_DECLINE':
strategy = {
type: 'manual_review',
urgency: 'low',
retryDelay: 0,
message: 'contact_bank',
maxRetries: 0
};
break;
default:
strategy = {
type: 'retry_escalate',
urgency: 'medium',
retryDelay: 2,
message: 'payment_failed_generic',
maxRetries: 3
};
}
return { charge, strategy };
Step 2: Implement Smart Retry Logic
Recharge has its own built-in retry schedule, but it is basic -- it retries at fixed intervals regardless of the failure reason. You can do better by orchestrating retries through the API based on your classification logic.
For insufficient funds declines, the optimal retry timing follows payroll cycles. Most consumers get paid biweekly on Fridays. Retrying on the 1st, 15th, or the Friday closest to the failure date recovers more charges than fixed 3-day intervals:
// Calculate optimal retry date based on failure reason
function getOptimalRetryDate(failureDate, errorType) {
const date = new Date(failureDate);
if (errorType === 'INSUFFICIENT_FUNDS') {
// Find next likely payroll date (1st, 15th, or nearest Friday)
const day = date.getDate();
const nextFirst = new Date(date);
const nextFifteenth = new Date(date);
const nextFriday = new Date(date);
nextFirst.setMonth(nextFirst.getMonth() + (day >= 1 ? 1 : 0));
nextFirst.setDate(1);
nextFifteenth.setDate(day >= 15 ? 15 : 15);
if (day >= 15) nextFifteenth.setMonth(nextFifteenth.getMonth() + 1);
// Find next Friday
const daysUntilFriday = (5 - date.getDay() + 7) % 7 || 7;
nextFriday.setDate(date.getDate() + daysUntilFriday);
// Return the earliest of the three that is at least 2 days away
const candidates = [nextFirst, nextFifteenth, nextFriday]
.filter(d => d - date >= 2 * 24 * 60 * 60 * 1000)
.sort((a, b) => a - b);
return candidates[0] || nextFriday;
}
// Default: retry in 2 days
date.setDate(date.getDate() + 2);
return date;
}
To execute the retry, use Recharge's charge endpoint:
// Retry a failed charge via Recharge API
const retryCharge = await fetch(
`https://api.rechargeapps.com/charges/${chargeId}/retry`,
{
method: 'POST',
headers: {
'X-Recharge-Access-Token': rechargeApiToken,
'Content-Type': 'application/json'
}
}
);
Step 3: Build the Dunning Email Sequence
While retries happen in the background, you need to communicate with the subscriber. The dunning email sequence is a parallel track that nudges the customer to update their payment method.
Here is the sequence that works well for most subscription businesses:
| Timing | Subject Line Pattern | Goal | |
|---|---|---|---|
| Immediately | Payment failed notification | "Quick heads up -- your payment didn't go through" | Inform, provide update link |
| Day 3 | Friendly reminder | "Your [Product] subscription is on hold" | Create urgency, easy update CTA |
| Day 7 | Value reminder | "Don't miss your next [Product] delivery" | Remind why they subscribed |
| Day 12 | Final warning | "Last chance to keep your subscription active" | Scarcity, final CTA |
| Day 14 | Cancellation notice | "Your subscription has been cancelled" | Win-back offer for reactivation |
Trigger these emails from your n8n workflow using Klaviyo's API (or whatever email platform you use):
// Trigger dunning email via Klaviyo
const triggerEmail = await fetch(
'https://a.klaviyo.com/api/events/',
{
method: 'POST',
headers: {
'Authorization': `Klaviyo-API-Key ${klaviyoApiKey}`,
'Content-Type': 'application/json',
'revision': '2024-02-15'
},
body: JSON.stringify({
data: {
type: 'event',
attributes: {
profile: {
data: {
type: 'profile',
attributes: {
email: subscriberEmail
}
}
},
metric: {
data: {
type: 'metric',
attributes: {
name: 'Subscription Payment Failed'
}
}
},
properties: {
dunning_step: 1,
failure_reason: errorType,
subscription_value: chargeTotal,
update_payment_url: paymentUpdateUrl,
product_name: productName,
retry_date: nextRetryDate
}
}
}
})
}
);
Key detail: Recharge generates a unique payment update URL for each customer. Include this link in every dunning email. It takes the subscriber directly to a page where they can enter a new card -- no login required, no hunting through account settings. The fewer clicks between "I should update my card" and "done," the higher your recovery rate.
Pre-Cancellation Win-Back Flows
When a subscriber clicks "Cancel" in their account portal, most stores show a confirmation dialog and process the cancellation. That is leaving money on the table. A pre-cancellation flow intercepts the cancel action and presents alternatives that address the subscriber's actual reason for leaving.
Capture the Cancellation Reason
The first step is understanding why they want to cancel. Configure your subscription portal to require a cancellation reason before processing. Recharge supports cancellation reason codes that you can customize. Common reasons and their automated responses:
// Map cancellation reasons to retention offers
const retentionOffers = {
'too_expensive': {
offer: 'discount',
action: 'apply_discount',
discount_percentage: 20,
discount_duration: 3, // months
message: "We get it -- budgets shift. How about 20% off your next 3 months?"
},
'too_much_product': {
offer: 'frequency_change',
action: 'reduce_frequency',
new_interval: 60, // days
message: "No problem. We can switch you to every 60 days instead."
},
'want_to_try_something_else': {
offer: 'product_swap',
action: 'show_swap_options',
message: "Totally fair. Want to swap to a different product instead of cancelling?"
},
'need_a_break': {
offer: 'pause',
action: 'pause_subscription',
pause_duration: 30, // days
message: "We'll pause your subscription for 30 days. It'll restart automatically -- or you can extend the pause anytime."
},
'other': {
offer: 'contact',
action: 'route_to_support',
message: "We'd love to help. Let us connect you with someone who can sort this out."
}
};
Implement the Retention Actions via API
When a subscriber accepts a retention offer, your workflow needs to execute the corresponding action. Here are the most common ones:
Apply a discount:
// Apply retention discount to subscription via Recharge
const applyDiscount = await fetch(
`https://api.rechargeapps.com/subscriptions/${subscriptionId}`,
{
method: 'PUT',
headers: {
'X-Recharge-Access-Token': rechargeApiToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
price: originalPrice * (1 - discountPercentage / 100)
})
}
);
// Schedule discount removal after retention period
// Store in your database: { subscriptionId, originalPrice, revertDate }
Skip upcoming charges:
// Skip the next N charges
const upcomingCharges = await fetch(
`https://api.rechargeapps.com/charges?customer_id=${customerId}&status=queued`,
{
headers: { 'X-Recharge-Access-Token': rechargeApiToken }
}
);
const charges = upcomingCharges.charges.slice(0, skipCount);
for (const charge of charges) {
await fetch(
`https://api.rechargeapps.com/charges/${charge.id}/skip`,
{
method: 'POST',
headers: {
'X-Recharge-Access-Token': rechargeApiToken,
'Content-Type': 'application/json'
}
}
);
}
Swap product:
// Swap the subscription product
const swapProduct = await fetch(
`https://api.rechargeapps.com/subscriptions/${subscriptionId}`,
{
method: 'PUT',
headers: {
'X-Recharge-Access-Token': rechargeApiToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
shopify_variant_id: newVariantId,
product_title: newProductTitle,
variant_title: newVariantTitle
})
}
);
Track Retention Offer Performance
Every retention offer should be logged so you can measure what works:
// Log retention offer interaction
const retentionLog = {
customer_id: customerId,
subscription_id: subscriptionId,
cancellation_reason: reason,
offer_presented: retentionOffers[reason].offer,
offer_accepted: accepted, // boolean
timestamp: new Date().toISOString(),
subscription_value_monthly: monthlyValue,
months_active: monthsSubscribed
};
// Push to your analytics database or Google Sheets
await fetch('https://your-n8n-instance.com/webhook/retention-log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(retentionLog)
});
Over time, this data tells you which offers save the most revenue by cancellation reason. You will typically find that "too expensive" subscribers respond well to discounts (40-50% save rate), while "too much product" subscribers almost always accept a frequency change (60-70% save rate). "Need a break" subscribers who pause instead of cancel reactivate at around 50%.
Building a Churn Prediction Pipeline
The most effective retention happens before the subscriber even considers cancelling. Churn prediction uses behavioral signals to identify at-risk subscribers so you can intervene proactively.
Signals That Predict Churn
These are the strongest leading indicators of subscription churn, ranked by predictive value:
- Skipped charges. A subscriber who skips two consecutive charges is 5x more likely to cancel within 30 days than one who has never skipped.
- Reduced engagement. If a subscriber stops opening your emails, stops visiting your site, or stops engaging with your content, they are disengaging. Pull email open rates from Klaviyo and site visit data from your analytics.
- Support tickets about billing. A subscriber who contacts support about charges, unexpected renewals, or "how to cancel" is signaling intent -- even if they do not cancel during that interaction.
- Failed payment history. Subscribers who have had a failed payment (even if recovered) churn at 2x the rate of those who have not.
- Subscription age. The highest churn risk is in months 2-4. Subscribers who make it past month 6 tend to stick around much longer.
Build the Risk Scoring Model
You do not need machine learning for effective churn prediction. A weighted scoring model based on these signals works surprisingly well:
// Calculate churn risk score for a subscriber
async function calculateChurnRisk(customerId) {
// Fetch subscriber data from Recharge
const subscriptions = await getSubscriptions(customerId);
const charges = await getChargeHistory(customerId);
const sub = subscriptions[0];
let riskScore = 0;
const signals = [];
// Signal 1: Recent skips
const recentSkips = charges
.filter(c => c.status === 'skipped')
.filter(c => daysSince(c.scheduled_at) <= 90)
.length;
if (recentSkips >= 2) {
riskScore += 30;
signals.push(`${recentSkips} skips in last 90 days`);
} else if (recentSkips === 1) {
riskScore += 10;
signals.push('1 skip in last 90 days');
}
// Signal 2: Failed payments
const failedCharges = charges
.filter(c => c.status === 'error' || c.last_charge_attempt_date)
.length;
if (failedCharges >= 2) {
riskScore += 25;
signals.push(`${failedCharges} payment failures in history`);
} else if (failedCharges === 1) {
riskScore += 10;
signals.push('1 previous payment failure');
}
// Signal 3: Subscription age (high risk in months 2-4)
const monthsActive = daysSince(sub.created_at) / 30;
if (monthsActive >= 1.5 && monthsActive <= 4) {
riskScore += 20;
signals.push(`In high-risk window (month ${Math.round(monthsActive)})`);
}
// Signal 4: No engagement (check Klaviyo)
const emailEngagement = await getEmailEngagement(sub.email);
if (emailEngagement.lastOpened && daysSince(emailEngagement.lastOpened) > 30) {
riskScore += 15;
signals.push('No email opens in 30+ days');
}
// Signal 5: Price sensitivity (has used discount before)
if (sub.price !== sub.original_price) {
// Currently on a discount -- risk of churn when it expires
riskScore += 10;
signals.push('Currently on retention discount');
}
return {
customerId,
email: sub.email,
riskScore,
riskLevel: riskScore >= 50 ? 'high' : riskScore >= 25 ? 'medium' : 'low',
signals,
monthsActive: Math.round(monthsActive),
ltv: charges.filter(c => c.status === 'success').length * parseFloat(sub.price)
};
}
Automate Proactive Interventions
Run the risk scoring model daily as a scheduled n8n workflow. For each risk level, trigger a different intervention:
Daily Churn Risk Assessment (n8n Cron: 8:00 AM)
-> Fetch all active subscribers from Recharge
-> Calculate risk score for each
-> Branch by risk level:
HIGH RISK (score 50+):
-> Send personalized email: "We noticed you skipped recently --
want to switch products or adjust frequency?"
-> Create task in CRM for manual outreach on high-LTV accounts
-> Tag subscriber in Recharge: "churn-risk-high"
MEDIUM RISK (score 25-49):
-> Enroll in re-engagement email sequence
-> Offer a surprise bonus item on next order
-> Tag subscriber: "churn-risk-medium"
LOW RISK (score under 25):
-> No action (continue normal email flows)
-> Log all scores to analytics dashboard
-> Alert ops team if high-risk count exceeds threshold
The proactive email for high-risk subscribers should feel personal, not automated. The subject line "Quick question about your subscription" outperforms "We miss you!" or "Don't cancel!" by a wide margin. Make it easy for the subscriber to take action -- include one-click links to skip, swap, or adjust frequency directly from the email.
Subscription Analytics Pipeline
You cannot improve what you do not measure. Build a simple analytics pipeline that tracks the health of your subscription program over time.
Key Metrics to Track
Set up a weekly pipeline that calculates and stores these metrics:
// Weekly subscription health metrics
const metrics = {
date: new Date().toISOString().split('T')[0],
// Growth
active_subscribers: activeCount,
new_subscribers: newThisWeek,
reactivations: reactivatedThisWeek,
// Churn
total_churned: churnedThisWeek,
voluntary_churn: voluntaryCancellations,
involuntary_churn: paymentFailureCancellations,
churn_rate: (churnedThisWeek / activeCount * 100).toFixed(2),
// Recovery
failed_charges: failedThisWeek,
recovered_charges: recoveredThisWeek,
recovery_rate: (recoveredThisWeek / failedThisWeek * 100).toFixed(2),
// Retention
retention_offers_presented: offerCount,
retention_offers_accepted: acceptedCount,
retention_save_rate: (acceptedCount / offerCount * 100).toFixed(2),
// Revenue
mrr: monthlyRecurringRevenue,
recovered_revenue: recoveredRevenue,
saved_revenue: savedFromRetentionOffers,
// Health
avg_subscription_age_months: avgAge,
subscribers_in_risk_window: riskWindowCount
};
Push these metrics to a Google Sheet or a dashboard tool like Metabase. The numbers you care about most are recovery rate (target: 50% or higher), retention save rate (target: 30% or higher), and involuntary churn rate (target: under 2% monthly).
The Complete Architecture
Here is the full system, showing how the components connect:
SUBSCRIPTION LIFECYCLE AUTOMATION
==================================
1. DUNNING MANAGEMENT
Recharge webhook: charge/failed
-> Classify failure type
-> Smart retry scheduling (payroll-aligned)
-> Dunning email sequence (5 emails over 14 days)
-> Retry via Recharge API at optimal intervals
-> If recovered: log success, update subscriber record
-> If not recovered after 14 days: cancel + win-back email
2. PRE-CANCELLATION RETENTION
Recharge webhook: subscription/cancelled (or portal intercept)
-> Capture cancellation reason
-> Present contextual retention offer
-> If accepted: execute offer (discount/skip/swap/pause)
-> Log offer + outcome for analytics
-> If rejected: process cancellation + enter win-back sequence
3. CHURN PREDICTION
n8n Cron: daily at 8:00 AM
-> Score all active subscribers
-> Trigger interventions by risk level
-> Tag subscribers in Recharge
-> Alert ops team on high-risk accounts
-> Log scores for trend analysis
4. ANALYTICS PIPELINE
n8n Cron: weekly on Monday at 6:00 AM
-> Calculate subscription health metrics
-> Push to Google Sheets / Metabase
-> Compare against targets
-> Alert if any metric exceeds threshold
-> Generate weekly subscription health report
5. WIN-BACK AUTOMATION
Triggered 30, 60, 90 days post-cancellation
-> Check if subscriber has reactivated (skip if yes)
-> Send win-back email with escalating offer
-> Day 30: "Come back" + 15% off
-> Day 60: "Still thinking about it?" + 25% off
-> Day 90: "Last chance" + 30% off + free bonus item
-> Track reactivation rates by offer tier
Common Mistakes to Avoid
A few patterns come up repeatedly when subscription automation goes wrong.
Retrying too aggressively. Retrying a declined card every day annoys the customer's bank and can trigger fraud flags that make future charges even less likely to succeed. Space retries at least 2-3 days apart and never exceed 4-5 total retry attempts.
Sending dunning emails that feel like threats. "Your subscription will be cancelled!" is technically true, but it creates anxiety instead of action. Frame dunning emails as helpful -- "Looks like your card needs updating. Here's a quick link to fix it." The subscriber is not your adversary. They probably want to keep the subscription and just have not gotten around to updating their card.
Offering discounts to everyone. If subscribers learn that attempting to cancel earns them a discount, you train them to cancel every few months. Reserve discounts for price-sensitive cancellation reasons and use non-monetary offers (skip, swap, pause, bonus item) for other reasons.
Ignoring the post-recovery experience. A subscriber whose payment just failed and was recovered needs reassurance, not silence. Send a brief confirmation email after a successful retry: "Good news -- your payment went through and your next delivery is on its way." This closes the loop and reduces support inquiries.
Not measuring what works. Without tracking which dunning emails get clicks, which retry timing recovers the most charges, and which retention offers have the highest save rate, you are guessing. Run the system for 30 days, analyze the data, then optimize. Even small improvements in recovery rate translate to significant revenue at scale.
Expected Results
For a store with 4,000 active subscribers and a $45 average subscription value, here is what a well-built system typically delivers:
| Metric | Before Automation | After Automation |
|---|---|---|
| Monthly involuntary churn | 3.0% | 1.2% |
| Failed payment recovery rate | 10% (manual) | 55% |
| Cancellation save rate | 0% (no intercept) | 35% |
| Monthly recovered revenue | ~$500 | ~$4,500 |
| Annual revenue impact | -- | +$48,000-65,000 |
The infrastructure cost is an n8n instance ($20-50/month), your existing Recharge subscription, and a Klaviyo account you are probably already paying for. The ROI shows up in the first billing cycle.
Start with dunning automation -- it is the highest-impact, lowest-effort piece. Get your failed payment recovery rate above 40% and you have already justified the build. Then layer on the pre-cancellation flow and churn prediction as your next iterations.
Subscription revenue is the most valuable revenue your business generates. Protecting it with automation is not a nice-to-have -- it is how you turn a leaky bucket into a growth engine.
Want to Talk Through Your Automation Needs?
Book a 30-minute call. We'll map out which automations would save you the most time — no obligation.
Want to Talk Through Your Automation Needs?
Book a 30-minute call. We'll map out which automations would save you the most time — no obligation.
Related Articles
How to Automate Slack Alerts for Airtable Updates Using Make.com
Step-by-step guide to building Make.com scenarios that watch Airtable for changes and push formatted Slack notifications automatically.
n8n + Klaviyo: Build Email Automations Klaviyo Can't Do Alone
Go beyond Klaviyo's native flows with n8n. Build advanced email automations using external triggers, cross-platform data, and smart segmentation.
Shopify to Google Sheets: Build a Free Real-Time Dashboard
Build live Shopify dashboards in Google Sheets using n8n webhooks — no paid apps needed. Step-by-step setup with ready-to-use templates.