Build a waste collection subscription by creating Paystack Plans for monthly billing, subscribing customers via the transaction initialize endpoint, scheduling collections when subscription.create fires, suspending collections when invoice.payment_failed enters the grace period, and resuming service when the next invoice.create success event arrives.
Tables: collection_plans (name, frequency, collections_per_month, price, paystack_plan_code), subscribers (name, phone, email, address, zone_id, plan_id, paystack_subscription_code, status, next_collection_date), collections (subscriber_id, driver_id, scheduled_date, status, completed_at, notes).
var PLANS = {
weekly: { collections: 4, price: 200000, label: 'Weekly — 4 collections/month' }, // NGN 2,000/month
biweekly: { collections: 2, price: 120000, label: 'Biweekly — 2 collections/month' }, // NGN 1,200/month
monthly: { collections: 1, price: 70000, label: 'Monthly — 1 collection/month' }, // NGN 700/month
};
async function subscribe(customerEmail, customerId, planName, address, zoneId) {
var plan = await db.collectionPlans.findByName(planName);
var res = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
email: customerEmail, amount: plan.price,
plan: plan.paystack_plan_code,
metadata: { customer_id: customerId, plan_id: plan.id, zone_id: zoneId },
}),
});
return (await res.json()).data.authorization_url;
}
// First subscription confirmed
if (event.event === 'subscription.create') {
var meta = event.data.metadata;
await db.subscribers.update(meta.customer_id, {
plan_id: meta.plan_id,
zone_id: meta.zone_id,
paystack_subscription_code: event.data.subscription_code,
status: 'active',
});
await scheduleCollectionsForCycle(meta.customer_id, meta.plan_id);
}
// Each billing cycle renewal
if (event.event === 'invoice.create' && event.data.status === 'success') {
var code = event.data.subscription.subscription_code;
var subscriber = await db.subscribers.findBySubscription(code);
if (subscriber) {
await db.subscribers.update(subscriber.id, { status: 'active' });
await scheduleCollectionsForCycle(subscriber.id, subscriber.plan_id);
}
}
async function scheduleCollectionsForCycle(subscriberId, planId) {
var plan = await db.collectionPlans.findById(planId);
var subscriber = await db.subscribers.findById(subscriberId);
var today = new Date();
for (var i = 0; i < plan.collections_per_month; i++) {
var collectionDate = getNextCollectionDay(subscriber.zone_id, today, i);
await db.collections.create({
subscriber_id: subscriberId, scheduled_date: collectionDate, status: 'scheduled',
});
}
}
// Build daily route for each zone
async function buildDriverRoute(zoneId, date) {
var collections = await db.collections.findByZoneAndDate(zoneId, date);
var sortedByAddress = collections.sort(function(a, b) {
return a.subscriber.address_order - b.subscriber.address_order;
});
return sortedByAddress;
}
// Driver marks collection complete
async function markCollected(driverId, collectionId, notes) {
await db.collections.update(collectionId, {
driver_id: driverId, status: 'completed',
completed_at: new Date(), notes: notes || '',
});
}
// Failed payment — enter grace period then suspend
if (event.event === 'invoice.payment_failed') {
var code = event.data.subscription.subscription_code;
var subscriber = await db.subscribers.findBySubscription(code);
await db.subscribers.update(subscriber.id, { status: 'grace_period' });
// Let current scheduled collections complete within 7 days
var graceCutoff = new Date();
graceCutoff.setDate(graceCutoff.getDate() + 7);
await sendSMS(subscriber.phone,
'Payment failed. Collections continue for 7 days. Pay to keep service: yourapp.com/billing'
);
}
// Cron: suspend after grace period
async function suspendGraceExpired() {
var graceDeadline = new Date();
graceDeadline.setDate(graceDeadline.getDate() - 7);
var gracePeriodSubs = await db.subscribers.findAll({ status: 'grace_period', updated_at_lt: graceDeadline });
for (var sub of gracePeriodSubs) {
await db.subscribers.update(sub.id, { status: 'suspended' });
await db.collections.cancelFuture(sub.id);
await sendSMS(sub.phone, 'Service suspended due to non-payment. Pay to restore: yourapp.com/billing');
}
}
if (event.event === 'subscription.disable') {
var code = event.data.subscription_code;
var subscriber = await db.subscribers.findBySubscription(code);
if (subscriber) {
await db.subscribers.update(subscriber.id, { status: 'cancelled' });
await db.collections.cancelFuture(subscriber.id);
}
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
See build a water delivery subscription service for the same recurring billing and route management pattern applied to water delivery.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course