
Subscription Billing
- 131 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Sell recurring subscriptions with automated billing, dunning emails for failed payments, plan proration, and self-serve cancellation.
About
Implements recurring subscription billing with dunning for failed payments, upgrade/downgrade proration, and self-serve cancellation. A developer uses it to run a subscription commerce model.
- Dunning emails for failed payments
- Plan upgrade/downgrade proration and self-serve cancellation
Subscription Billing by the numbers
- 131 all-time installs (skills.sh)
- Ranked #2,723 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill subscription-billingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 131 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Sell recurring subscriptions with automated billing, dunning emails for failed payments, plan proration, and self-serve cancellation.
Files
Subscription Billing
Overview
Subscription billing lets customers pay automatically on a recurring schedule — weekly, monthly, or annually. Shopify, WooCommerce, and BigCommerce each have dedicated subscription apps and plugins that handle the full lifecycle: initial checkout, recurring billing, failed payment recovery (dunning), plan changes, pausing, and cancellation. For headless storefronts, Stripe Subscriptions provides the same functionality via API with no need to build billing logic from scratch.
When to Use This Skill
- When building a subscription box, membership, or "Subscribe & Save" program
- When the current subscription logic is manual and not scaling with customer count
- When dunning (failed payment recovery) is not automated and causing unnecessary churn
- When customers need self-serve plan upgrades, downgrades, pausing, and cancellation
Core Instructions
Step 1: Determine your platform and choose the right subscription tool
| Platform | Recommended Tool | Notes |
|---|---|---|
| Shopify | Recharge or Seal Subscriptions or Bold Subscriptions (Shopify App Store) | Shopify has a native Subscriptions API; Recharge is the most widely used third-party app; Seal is a newer lower-cost option |
| Shopify (simple) | Shopify Subscriptions (built-in, free) | For basic subscription needs — launched 2024; handles simple recurring orders |
| WooCommerce | WooCommerce Subscriptions ($199/year, official WooCommerce extension) | The official and most complete WooCommerce subscription solution; handles all billing, dunning, and management |
| BigCommerce | Recurly (via BigCommerce App Marketplace) or Rebilly | BigCommerce does not have native subscriptions; third-party apps handle it |
| Custom / Headless | Stripe Subscriptions | Stripe Subscriptions handles the complete recurring billing lifecycle with webhooks |
Step 2: Set up subscriptions on your platform
---
Shopify (Shopify Subscriptions — built-in, free)
For basic subscription products:
1. Go to Settings → Apps and sales channels → Shopify Subscriptions (may need to install from App Store) 2. Go to Products and open any product you want to make subscribable 3. Under Purchase options, click Add subscription plan 4. Configure:
- Billing frequency: weekly, monthly, every X months
- Discount for subscribers (e.g., 10% off vs. one-time purchase)
- Free trial period (optional)
5. The product page will show both "One-time purchase" and "Subscribe & Save" options
Shopify Subscriptions handles: recurring billing, payment failure emails, and a customer portal for managing subscriptions. For advanced dunning, analytics, and subscriber portals, use Recharge or Seal Subscriptions.
Shopify (Recharge — recommended for scale)
1. Install Recharge from the Shopify App Store 2. Connect your Shopify store and Stripe account in Recharge → Settings → Payment Processors 3. Configure subscription products in Recharge → Products → Add product:
- Select which Shopify products can be purchased as subscriptions
- Set billing intervals (weekly, monthly, every 2 months, etc.)
- Configure subscriber discounts
4. Set up the customer portal: go to Recharge → Customer Portal to configure what subscribers can manage themselves (pause, skip, change date, cancel, swap product) 5. Configure dunning: go to Recharge → Settings → Dunning and set up the retry schedule and email sequence for failed payments
Recharge dunning defaults:
- Day 0: First payment failure → email customer
- Day 3: Retry payment → email customer
- Day 7: Retry payment → final warning email
- Day 14: Retry payment → subscription cancelled if still failed
WooCommerce (WooCommerce Subscriptions)
1. Purchase and install WooCommerce Subscriptions from woocommerce.com/products/woocommerce-subscriptions 2. Create a subscription product: go to Products → Add New and set product type to Simple subscription or Variable subscription 3. Configure under Subscription data:
- Billing period: daily, weekly, monthly, annually
- Billing interval: every 1, 2, 3... periods
- Subscription length: ongoing or limited (e.g., 12 months)
- Sign-up fee (optional)
- Free trial period (optional)
- Subscriber discount (set via regular/sale price comparison)
4. Configure payment methods: go to WooCommerce → Settings → Payments and ensure your gateway supports recurring payments (Stripe via WooCommerce Stripe plugin, PayPal via WooCommerce PayPal Payments) 5. Configure dunning: go to WooCommerce → Settings → Subscriptions → Failing payments and set retry rules and email templates
Customer portal for WooCommerce: Customers manage subscriptions from their My Account → Subscriptions page. WooCommerce Subscriptions provides pause, cancel, and payment method update functionality there by default.
BigCommerce (Recurly)
1. Sign up at recurly.com and create a plan (monthly, annual, etc.) with your pricing 2. Install the Recurly app from the BigCommerce App Marketplace and connect your store 3. Create subscription products that link to your Recurly plans 4. Recurly handles: checkout, recurring billing, dunning, invoicing, and the subscriber portal 5. Configure dunning in Recurly → Configuration → Dunning Campaigns: set retry schedule and email content
---
Custom / Headless
Use Stripe Subscriptions for the full recurring billing lifecycle:
Create a subscription plan: In the Stripe Dashboard → Products, create a product with a recurring price (e.g., "Monthly Plan — $29/month"). Copy the Price ID (starts with price_).
Subscribe a customer:
// Create or retrieve the Stripe customer
const customer = await stripe.customers.create({
email: customerEmail,
payment_method: paymentMethodId,
invoice_settings: { default_payment_method: paymentMethodId },
});
// Create the subscription
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: 'price_monthly_pro_29' }], // Price ID from Stripe Dashboard
payment_behavior: 'default_incomplete', // Create subscription, then confirm payment
payment_settings: { save_default_payment_method: 'on_subscription' },
expand: ['latest_invoice.payment_intent'],
trial_period_days: 14, // Optional trial
metadata: { customer_id: customerId },
});
// Return client_secret for 3DS confirmation if needed
const clientSecret = subscription.latest_invoice?.payment_intent?.client_secret;
res.json({ subscriptionId: subscription.id, clientSecret });Sync subscription status via webhooks:
// Webhook handler for subscription events
async function handleSubscriptionWebhook(event) {
switch (event.type) {
case 'customer.subscription.updated':
await syncSubscriptionStatus(event.data.object);
break;
case 'customer.subscription.deleted':
// Subscription cancelled — revoke access
await db.subscriptions.update({
where: { stripeSubscriptionId: event.data.object.id },
data: { status: 'cancelled', cancelledAt: new Date() },
});
break;
case 'invoice.payment_succeeded':
await recordSuccessfulBillingCycle(event.data.object);
break;
case 'invoice.payment_failed':
// Send dunning email — Stripe retries automatically per your settings
await sendDunningEmail(event.data.object);
break;
case 'customer.subscription.trial_will_end':
// Send trial ending email 3 days before trial ends
await sendTrialEndingEmail(event.data.object);
break;
}
}Configure dunning in Stripe Dashboard: Go to Stripe Dashboard → Billing → Settings → Smart Retries. Enable Smart Retries — Stripe's ML-based retry timing outperforms fixed schedules. Also configure automatic subscription cancellation after all retries fail under Stripe Dashboard → Billing → Settings → Automatic collection.
Plan changes with prorations:
// Upgrade or downgrade a subscription
const subscription = await stripe.subscriptions.retrieve(stripeSubscriptionId);
await stripe.subscriptions.update(stripeSubscriptionId, {
items: [{ id: subscription.items.data[0].id, price: newPriceId }],
proration_behavior: 'create_prorations', // Charge/credit the difference immediately
});
// Stripe creates a prorated invoice automaticallyCancellation with end-of-period access:
// Cancel at period end — customer retains access until the next billing date
await stripe.subscriptions.update(stripeSubscriptionId, {
cancel_at_period_end: true,
});
// Immediate cancellation
await stripe.subscriptions.cancel(stripeSubscriptionId);Best Practices
- Use platform-native subscription tools — WooCommerce Subscriptions, Recharge on Shopify, and Stripe Subscriptions are all production-hardened and handle edge cases (failed payments, prorations, tax changes) correctly
- Never store subscription state only in Stripe — always sync subscription status to your database via webhooks; do not call Stripe on every page load to check status
- Configure Smart Retries in Stripe — enable under Stripe Dashboard → Billing → Settings; ML-based retry timing recovers significantly more failed payments than fixed schedules
- Send dunning emails at each retry — escalate urgency: "payment failed" → "account at risk" → "access will be suspended"; include a direct link to update payment method
- Let customers self-serve — build a customer portal (or use Stripe's hosted Customer Portal at Stripe Dashboard → Billing → Customer portal) for plan changes, pausing, and cancellation; reduces support load
- Prorate upgrades immediately; apply downgrades at period end — upgrades should charge the difference now; downgrades should apply at the next renewal to avoid complex partial refunds
Common Pitfalls
| Problem | Solution |
|---|---|
| Subscription shows as active after cancellation | Sync status via webhooks — customer.subscription.deleted must trigger a database update; never poll Stripe for status on page load |
| WooCommerce Subscriptions not retrying failed payments | Verify the payment gateway supports tokenized recurring payments; Stripe via the WooCommerce Stripe plugin supports this; basic PayPal Standard does not |
| Trial converts to paid without customer knowing | Send the trial_will_end email 3 days before (Stripe fires the webhook automatically; Recharge and WooCommerce Subscriptions do this by default) |
| Duplicate dunning emails on retried webhooks | Check that the invoice attempt count matches the last dunning email you sent before sending another; use the invoice ID + attempt count as the idempotency key |
| Proration charges customer unexpectedly on upgrade | Show the upcoming invoice preview before applying plan changes (Stripe: use stripe.invoices.retrieveUpcoming()); show the proration amount to the customer first |
Related Skills
- @stripe-integration
- @order-processing-pipeline
- @tax-calculation
- @invoice-generation-automation
{
"context": "Tests whether the agent implements escalating dunning emails keyed to attempt_count, prevents duplicate emails by checking invoice status and attempt tracking, handles immediate vs. period-end cancellation correctly, stores cancellation reason, sends a winback email for period-end cancellations, and revoking access on subscription deletion.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Escalating email templates",
"max_score": 10,
"description": "Uses distinct email templates (or content) for attempt 1, attempt 2, and attempt 3+, where the messaging escalates in urgency"
},
{
"name": "attempt_count drives template",
"max_score": 8,
"description": "Uses `invoice.attempt_count` to select which dunning email template/content to send"
},
{
"name": "Update payment link in email",
"max_score": 6,
"description": "Dunning email data includes a direct link for the customer to update their payment method"
},
{
"name": "Invoice URL in email",
"max_score": 6,
"description": "Dunning email data includes the `hosted_invoice_url` (or equivalent invoice link) from the Stripe invoice object"
},
{
"name": "Duplicate email guard: invoice.status",
"max_score": 10,
"description": "Checks that `invoice.status !== 'paid'` (or equivalent) before sending dunning email to avoid re-sending on already-paid invoices"
},
{
"name": "Duplicate email guard: attempt tracking",
"max_score": 10,
"description": "Guards against duplicate sends by checking whether an email has already been sent for this specific `attempt_count` (e.g., comparing stored `emailSentForAttempt` to `invoice.attempt_count`)"
},
{
"name": "Cancel at period end by default",
"max_score": 10,
"description": "When `immediately` is false (or absent), sets `cancel_at_period_end: true` on the Stripe subscription rather than immediately canceling"
},
{
"name": "Immediate cancel uses cancel()",
"max_score": 8,
"description": "When `immediately` is true, calls `stripe.subscriptions.cancel()` rather than updating `cancel_at_period_end`"
},
{
"name": "Cancellation reason stored",
"max_score": 7,
"description": "Stores the cancellation `reason` in the local database subscription record"
},
{
"name": "Winback email on period-end cancel",
"max_score": 10,
"description": "Sends a winback / retention offer email when `immediately` is false (i.e., when canceling at period end)"
},
{
"name": "Revoke access on deletion",
"max_score": 8,
"description": "The `customer.subscription.deleted` webhook handler calls a function to revoke access (e.g., `revokeSubscriptionAccess` or equivalent) in addition to updating the DB status"
},
{
"name": "IMPLEMENTATION_NOTES: idempotency explanation",
"max_score": 7,
"description": "`IMPLEMENTATION_NOTES.md` explains how duplicate dunning emails are prevented"
}
]
}
Failed Payment Recovery and Subscription Cancellation
Problem/Feature Description
A subscription e-commerce company is losing roughly 8% of its monthly revenue to involuntary churn — subscriptions lapsing because payment methods expired or had insufficient funds. At the same time, their webhook handler is sending duplicate recovery emails when Stripe re-delivers webhook events, which is angering customers and damaging their sender reputation.
The engineering team needs to build two things: (1) a robust invoice.payment_failed webhook handler that sends appropriately escalating recovery emails without ever sending duplicates, and (2) a cancellation endpoint that records why customers are leaving and makes a last-ditch attempt to retain them.
The company wants the emails to match customer urgency: a gentle first notice, a more urgent second attempt, and a final warning — with each email containing enough context and actionable information for the customer to resolve the issue quickly. The cancellation flow should preserve the customer's access until the end of their paid period (unless they specifically request immediate termination), and attempt to win them back with an offer email.
Output Specification
Write the following files:
1. api/webhooks/subscription-events.js — exports handleSubscriptionEvents(event) that handles at minimum:
invoice.payment_failed— sends escalating dunning email, safely (no duplicates)customer.subscription.deleted— marks subscription canceled in DB and revokes access
2. api/subscriptions/cancel.js — exports async function cancelSubscription(req, res) accepting:
{ "subscriptionId": "...", "immediately": false, "reason": "too_expensive" }3. IMPLEMENTATION_NOTES.md explaining:
- The dunning email escalation logic and how duplicate sends are prevented
- How cancellation timing works (immediate vs. period-end)
- How the winback offer fits into the cancellation flow
{
"context": "Tests whether the agent implements plan changes using stripe.invoices.retrieveUpcoming for proration preview, uses create_prorations as the default behavior, applies upgrades immediately with prorations while scheduling downgrades for end of billing period, and documents the approach clearly.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Preview uses retrieveUpcoming",
"max_score": 15,
"description": "Uses `stripe.invoices.retrieveUpcoming` (or equivalent upcoming invoice API) to calculate the proration cost in preview mode"
},
{
"name": "Preview returns proration amount",
"max_score": 8,
"description": "Preview mode returns the proration amount due (e.g., `prorationAmount` or equivalent field) derived from the upcoming invoice"
},
{
"name": "create_prorations default",
"max_score": 12,
"description": "Uses `proration_behavior: 'create_prorations'` (or equivalent) as the default when applying a plan change via `stripe.subscriptions.update`"
},
{
"name": "Upgrade prorated immediately",
"max_score": 12,
"description": "Upgrade path applies the plan change immediately with proration (does NOT use `none` proration_behavior or schedule for end of period)"
},
{
"name": "Downgrade at period end",
"max_score": 15,
"description": "Downgrade path is scheduled to take effect at the end of the current billing period (e.g., sets `proration_behavior: 'none'` or uses `billing_cycle_anchor`, or schedules a subscription schedule) — does NOT immediately prorate a downgrade"
},
{
"name": "Applies via subscriptions.update",
"max_score": 10,
"description": "Uses `stripe.subscriptions.update` with an `items` array containing the new `price` to apply the plan change"
},
{
"name": "IMPLEMENTATION_NOTES: proration explanation",
"max_score": 15,
"description": "`IMPLEMENTATION_NOTES.md` explicitly explains the different behavior for upgrades vs. downgrades with respect to proration"
},
{
"name": "IMPLEMENTATION_NOTES: preview vs apply",
"max_score": 13,
"description": "`IMPLEMENTATION_NOTES.md` documents what preview mode returns vs. what the apply mode does"
}
]
}
Subscription Plan Change Endpoint
Problem/Feature Description
A SaaS company offers three tiers: Starter ($29/mo), Growth ($79/mo), and Enterprise ($199/mo). Customers frequently want to upgrade mid-billing-cycle when they outgrow their current plan, or downgrade when budgets are cut. The product team has received complaints that customers are surprised by unexpected charges when they upgrade — they didn't know how much they'd be billed immediately for the partial month.
The engineering team needs to build a plan-change API that solves two problems: first, a "preview" mode that lets the frontend show customers exactly what they'll be charged before they commit; second, the actual plan-change operation. The endpoint also needs to handle the difference between upgrades (which should charge/credit immediately for the partial cycle) and downgrades (which should take effect at the next renewal to avoid complicated partial refunds).
The database record for the subscription must be kept in sync, and the endpoint should be robust to re-use across both upgrade and downgrade scenarios.
Output Specification
Write the plan-change endpoint as api/subscriptions/change-plan.js, exporting async function changePlan(req, res).
The endpoint should accept:
subscriptionId— the internal DB subscription IDnewPlanId— the Stripe price ID to switch todirection—"upgrade"or"downgrade"- Optional query parameter
?preview=trueto return cost preview without applying
Also write IMPLEMENTATION_NOTES.md documenting:
- How proration is handled for upgrades vs. downgrades
- What is returned in preview mode vs. apply mode
- Any edge cases or important design decisions
{
"context": "Tests whether the agent correctly implements a Stripe subscription creation endpoint using default_incomplete payment behavior, expands the payment intent for 3DS, saves the default payment method, stores metadata, syncs subscription data to the local database including trialEnd, and returns the client secret to the frontend.",
"type": "weighted_checklist",
"checklist": [
{
"name": "default_incomplete behavior",
"max_score": 12,
"description": "Sets `payment_behavior: 'default_incomplete'` when calling `stripe.subscriptions.create`"
},
{
"name": "save_default_payment_method",
"max_score": 8,
"description": "Includes `payment_settings: { save_default_payment_method: 'on_subscription' }` in the subscription creation params"
},
{
"name": "Expand payment intent",
"max_score": 12,
"description": "Includes `expand: ['latest_invoice.payment_intent']` in the subscription creation params"
},
{
"name": "Returns client secret",
"max_score": 8,
"description": "Extracts and returns the `client_secret` from `subscription.latest_invoice.payment_intent` in the response"
},
{
"name": "Subscription metadata",
"max_score": 5,
"description": "Sets `metadata` on the subscription containing a customer identifier (e.g., `customer_id`)"
},
{
"name": "Trial period support",
"max_score": 8,
"description": "Uses `trial_period_days` parameter on the subscription when `trialDays > 0`"
},
{
"name": "DB record: core fields",
"max_score": 12,
"description": "Persists subscription to local DB with at minimum: stripeSubscriptionId, stripeCustomerId, planId, and status"
},
{
"name": "DB record: period dates",
"max_score": 8,
"description": "Stores `currentPeriodStart` and `currentPeriodEnd` converted from Stripe Unix timestamps to Date objects (multiplied by 1000)"
},
{
"name": "DB record: trialEnd",
"max_score": 8,
"description": "Stores `trialEnd` in the DB record (null when no trial, Date when trial exists)"
},
{
"name": "DB upsert pattern",
"max_score": 7,
"description": "Uses an upsert operation (not a plain insert) keyed on `stripeSubscriptionId` to prevent duplicate records"
},
{
"name": "IMPLEMENTATION_NOTES present",
"max_score": 12,
"description": "File `IMPLEMENTATION_NOTES.md` exists and mentions at least two of: payment_behavior, client_secret/3DS, database sync, trial handling"
}
]
}
New SaaS Subscription Sign-Up Endpoint
Problem/Feature Description
A B2B SaaS company is launching a new project management tool. They currently collect payment details on a sign-up form using Stripe Elements (which gives them a paymentMethodId) and want to offer new customers a 14-day free trial before their first charge. After the trial, customers are billed monthly.
The engineering team needs a backend API endpoint that handles the full subscription creation flow: looking up or creating a Stripe customer, attaching the provided payment method, and creating the Stripe subscription. The endpoint must return enough information for the frontend to handle any additional payment authentication steps (e.g., 3D Secure).
Critically, the subscription data must be stored in the company's own PostgreSQL database immediately after being created in Stripe, so that the rest of the application can check subscription status without making live calls to Stripe on every request.
Output Specification
Write the subscription creation endpoint as a JavaScript file at api/subscriptions/create.js. The file should export an async function createSubscription(req, res) that accepts the following JSON body:
{
"planId": "price_xxx",
"paymentMethodId": "pm_xxx",
"email": "user@example.com",
"trialDays": 14
}Also write a short IMPLEMENTATION_NOTES.md explaining the key architectural decisions made in your implementation, specifically: how the Stripe subscription is configured, what is stored in the database, and what is returned to the frontend.
{
"name": "finsi/subscription-billing",
"version": "0.1.0",
"summary": "Recurring payment flows with dunning, plan changes, prorations, and cancellation",
"skills": {
"subscription-billing": {
"path": "SKILL.md"
}
}
}