Subscription/recurring billing introduces genuine complexity beyond one-time transactions — managing billing cycles, failed payment retries, and subscription lifecycle. This guide covers implementing this on self-hosted infrastructure.
Why Subscription Billing Is More Complex Than One-Time Purchases
Beyond the initial charge, you need to manage recurring billing cycles, handle failed payment retries, process upgrades/downgrades/cancellations, and maintain accurate subscription state over an extended, ongoing customer relationship — genuinely more involved than a single transaction.
Using Your Payment Gateway's Native Subscription Support
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
});
Most major payment gateways provide built-in subscription management — handling the recurring billing logic, retry attempts, and much of the lifecycle complexity, rather than building this from scratch.
Handling Subscription Webhooks
switch (event.type) {
case 'invoice.payment_succeeded':
await extendSubscriptionAccess(event.data.subscriptionId);
break;
case 'invoice.payment_failed':
await handleFailedPayment(event.data.subscriptionId);
break;
case 'customer.subscription.deleted':
await revokeAccess(event.data.subscriptionId);
break;
}
See How to Configure Payment Gateway Webhooks Securely for the underlying webhook security pattern — your application's subscription state should be driven by these authoritative webhook events, not assumptions.
Handling Failed Payment Retry Logic (Dunning)
"Dunning" refers to the process of retrying failed payments and communicating with customers about payment issues — most payment gateways have configurable dunning logic (retry schedule, customer email notifications); understand and appropriately configure this rather than accepting default settings blindly.
Managing Access Based on Subscription Status
async function checkAccess(userId) {
const subscription = await getActiveSubscription(userId);
return subscription && subscription.status === 'active';
}
Access control should check genuine current subscription status (synced from webhook events) rather than assuming access based on a one-time initial purchase confirmation.
Handling Upgrades and Downgrades
await stripe.subscriptions.update(subscriptionId, {
items: [{ id: subscriptionItemId, price: newPriceId }],
proration_behavior: 'create_prorations',
});
Most gateways support prorated billing for mid-cycle plan changes — understand your specific business's desired proration policy and configure accordingly, since the default behavior may not match your intended billing approach.
Handling Cancellations Gracefully
Decide whether cancellation should be immediate or take effect at the end of the current billing period (the more common, customer-friendly approach, since the customer has already paid for that period) — implement consistently with your stated cancellation policy.
Sending Renewal and Billing Notifications
See How to Configure Email Sending for Order Notifications (Transactional Email) — proactive communication (upcoming renewal, payment failure, successful renewal) reduces customer confusion and support burden compared to purely silent automatic billing.
Reconciling Subscription State Periodically
Beyond relying purely on real-time webhooks, periodic reconciliation (comparing your application's subscription state against the payment gateway's authoritative state) catches any drift from missed/failed webhook processing.
Common Errors
Customer retains access despite a failed/cancelled subscription — verify your access control logic correctly reflects current subscription status from webhook-driven state, not a cached or stale assumption from the original subscription creation.
Continue Reading
- How to Configure Payment Gateway Webhooks Securely
- How to Configure Email Sending for Order Notifications (Transactional Email)
- How to Implement Idempotent API Endpoints
Browse more articles in E-commerce Platform Deployment.