Build a bill splitting app by letting one person create a bill with total amount and participants, calculating each person's share, generating individual Paystack payment links for each participant, and tracking payments via webhooks until the bill is fully settled. The bill creator sees live payment status for each participant.
Tables: bills (title, total_amount, creator_id, status, created_at), bill_participants (bill_id, name, email, phone, share_amount, status, paystack_ref, paid_at).
async function createBill(creatorId, title, totalAmount, participants) {
var bill = await db.bills.create({ title, total_amount: totalAmount, creator_id: creatorId, status: 'open' });
// Default: equal split. Creator can customize amounts
var equalShare = Math.floor(totalAmount / participants.length);
var remainder = totalAmount - (equalShare * participants.length);
for (var i = 0; i < participants.length; i++) {
var share = equalShare + (i === 0 ? remainder : 0); // give remainder to first person
var reference = 'BILL-' + bill.id + '-P' + i + '-' + Date.now();
await db.billParticipants.create({
bill_id: bill.id,
name: participants[i].name,
email: participants[i].email,
phone: participants[i].phone,
share_amount: share,
status: 'pending',
paystack_ref: reference,
});
// Pre-create Paystack payment link for each participant
await sendPaymentLinkToParticipant(participants[i], share, reference, bill.title);
}
return bill;
}
async function sendPaymentLinkToParticipant(participant, amount, reference, billTitle) {
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: participant.email, amount, currency: 'NGN', reference,
metadata: { participant_phone: participant.phone, bill_title: billTitle },
}),
});
var link = (await res.json()).data.authorization_url;
// Send via WhatsApp (preferred in Nigeria) or SMS
await sendWhatsApp(participant.phone, 'You owe NGN ' + (amount / 100) + ' for "' + billTitle + '". Pay here: ' + link);
}
// Webhook: mark participant as paid
if (event.event === 'charge.success') {
var participant = await db.billParticipants.findByRef(event.data.reference);
if (!participant) return res.sendStatus(200);
await db.billParticipants.update(participant.id, { status: 'paid', paid_at: new Date() });
var bill = await db.bills.findById(participant.bill_id);
var allParticipants = await db.billParticipants.findAll({ bill_id: bill.id });
var allPaid = allParticipants.every(p => p.status === 'paid');
if (allPaid) {
await db.bills.update(bill.id, { status: 'settled' });
await notifyCreator(bill.creator_id, 'All payments received! Bill "' + bill.title + '" is fully settled.');
} else {
var paidCount = allParticipants.filter(p => p.status === 'paid').length;
await notifyCreator(bill.creator_id, paidCount + '/' + allParticipants.length + ' paid for "' + bill.title + '".');
}
}
// Get bill status for creator dashboard
async function getBillStatus(billId) {
var bill = await db.bills.findById(billId);
var participants = await db.billParticipants.findAll({ bill_id: billId });
return {
...bill,
participants: participants.map(p => ({ name: p.name, share: p.share_amount, status: p.status, paid_at: p.paid_at })),
paid_total: participants.filter(p => p.status === 'paid').reduce((sum, p) => sum + p.share_amount, 0),
remaining: participants.filter(p => p.status === 'pending').reduce((sum, p) => sum + p.share_amount, 0),
};
}
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 group contribution app for a related pattern where group members make recurring contributions.
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