Build a group contribution app by creating contribution cycles (monthly), sending each member a Paystack payment link for their fixed contribution, tracking payments per member per cycle, and disbursing the pooled amount to the designated member when all (or a quorum) have contributed. Use webhooks to track each member's payment as it arrives.
Tables: groups (name, contribution_amount, cycle_frequency, payout_order[], created_by), members (group_id, user_id, name, email, phone, bank_account, paystack_recipient_code, payout_position), cycles (group_id, cycle_number, month, year, recipient_member_id, status, total_collected), contributions (cycle_id, member_id, amount, paystack_ref, paid_at).
async function startNewCycle(groupId) {
var group = await db.groups.findById(groupId);
var lastCycle = await db.cycles.findLatest(groupId);
var cycleNumber = (lastCycle?.cycle_number || 0) + 1;
var recipientIndex = (cycleNumber - 1) % group.payout_order.length;
var recipientMemberId = group.payout_order[recipientIndex];
var now = new Date();
var cycle = await db.cycles.create({
group_id: groupId,
cycle_number: cycleNumber,
month: now.getMonth() + 1,
year: now.getFullYear(),
recipient_member_id: recipientMemberId,
status: 'open',
total_collected: 0,
});
// Send payment links to all members
var members = await db.members.findByGroup(groupId);
for (var member of members) {
var reference = 'CHAMA-' + cycle.id + '-' + member.id + '-' + Date.now();
await db.contributions.create({ cycle_id: cycle.id, member_id: member.id, paystack_ref: reference, status: 'pending' });
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: member.email,
amount: group.contribution_amount,
currency: 'NGN',
reference,
metadata: { cycle_id: cycle.id, member_id: member.id, group_id: groupId },
}),
});
var link = (await res.json()).data.authorization_url;
await sendContributionRequest(member.phone, link, group.name, group.contribution_amount / 100);
}
return cycle;
}
if (event.event === 'charge.success') {
var meta = event.data.metadata;
var contribution = await db.contributions.findByRef(event.data.reference);
await db.contributions.update(contribution.id, { status: 'paid', paid_at: new Date() });
await db.cycles.increment(meta.cycle_id, 'total_collected', event.data.amount);
// Check if cycle is fully funded or deadline passed
var cycle = await db.cycles.findById(meta.cycle_id);
var group = await db.groups.findById(meta.group_id);
var members = await db.members.findByGroup(meta.group_id);
var paidCount = await db.contributions.countPaid(meta.cycle_id);
if (paidCount >= members.length) {
await disburseCycle(cycle, group);
}
}
async function disburseCycle(cycle, group) {
var recipient = await db.members.findById(cycle.recipient_member_id);
// Ensure recipient has a Paystack transfer recipient code
if (!recipient.paystack_recipient_code) {
var res = await fetch('https://api.paystack.co/transferrecipient', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'nuban', name: recipient.name, account_number: recipient.account_number, bank_code: recipient.bank_code, currency: 'NGN' }),
});
var code = (await res.json()).data.recipient_code;
await db.members.update(recipient.id, { paystack_recipient_code: code });
recipient.paystack_recipient_code = code;
}
await fetch('https://api.paystack.co/transfer', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
source: 'balance',
amount: cycle.total_collected,
recipient: recipient.paystack_recipient_code,
reason: group.name + ' — Cycle ' + cycle.cycle_number + ' payout',
}),
});
await db.cycles.update(cycle.id, { status: 'disbursed' });
await notifyAllMembers(group.id, recipient.name, cycle.total_collected);
}
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 bill splitting app for the simpler per-person payment link pattern without the rotation payout structure.
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