Build a Bill Splitting App
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.
Bill Creation and Share Calculation
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);
}
Payment Tracking and Bill Settlement
// 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),
};
}
Learn More
See build a group contribution app for a related pattern where group members make recurring contributions.
Key Takeaways
- ✓Generate one Paystack payment link per participant — this allows individual tracking of who has paid.
- ✓Shares can be equal or custom amounts — let the bill creator decide how to divide.
- ✓Send WhatsApp or SMS payment link to each participant — not just email.
- ✓The bill creator sees a live dashboard: 2/5 paid, 3/5 pending.
- ✓Send automatic reminders to participants who have not paid after 24 hours.
Frequently Asked Questions
- What if someone claims they cannot afford their share?
- The app facilitates payment; it does not enforce it. If someone cannot or will not pay, the bill creator has two options: adjust the remaining participants' shares to cover the gap, or chase the person outside the app. Consider adding a "Request adjustment" feature where the creator can edit individual shares after the fact.
- Can the bill creator pay on behalf of someone else?
- Yes. Add a "Pay for [Person]" button on the bill dashboard that lets the creator make a Paystack payment using that participant's reference. When the webhook fires, it marks that participant as paid — just as if they had paid themselves.
- How do I handle the case where someone pays twice accidentally?
- Make your webhook handler idempotent: check if the participant is already marked as paid before processing the event. If already paid, acknowledge the webhook (return 200) but take no action. Refund the second payment using the Paystack Refund API with the duplicate transaction reference.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon