Build a Microfinance Repayment Portal
Build a microfinance repayment portal by disbursing loans via the Paystack Transfers API to borrowers' bank accounts, generating a repayment schedule on disbursement, sending a Paystack payment link for each repayment due, and optionally using card tokenization (authorization_code from first repayment) for auto-debit on subsequent installments.
Loan Disbursement and Repayment Schedule
Tables: borrowers (name, phone, id_number, bank_code, account_number, paystack_recipient_code, credit_score), loans (borrower_id, principal, interest_rate, term_weeks, weekly_installment, status, disbursed_at), repayments (loan_id, installment_number, due_date, amount_due, amount_paid, status, paystack_ref, paid_at).
function calculateInstallment(principal, interestRate, termWeeks) {
var totalInterest = principal * interestRate;
var total = principal + totalInterest;
return Math.ceil(total / termWeeks);
}
async function disburseLoan(loanId) {
var loan = await db.loans.findById(loanId);
var borrower = await db.borrowers.findById(loan.borrower_id);
// Ensure recipient code exists
if (!borrower.paystack_recipient_code) {
var rcpRes = 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: borrower.name, account_number: borrower.account_number, bank_code: borrower.bank_code, currency: 'NGN' }),
});
var code = (await rcpRes.json()).data.recipient_code;
await db.borrowers.update(borrower.id, { paystack_recipient_code: code });
borrower.paystack_recipient_code = code;
}
// Disburse
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: loan.principal, recipient: borrower.paystack_recipient_code, reason: 'Loan disbursement #' + loanId }),
});
await db.loans.update(loanId, { status: 'disbursed', disbursed_at: new Date() });
// Generate repayment schedule
var installment = calculateInstallment(loan.principal, loan.interest_rate, loan.term_weeks);
for (var i = 1; i <= loan.term_weeks; i++) {
var dueDate = new Date();
dueDate.setDate(dueDate.getDate() + (i * 7));
var reference = 'REPAY-' + loanId + '-' + i;
await db.repayments.create({ loan_id: loanId, installment_number: i, due_date: dueDate, amount_due: installment, status: 'pending', paystack_ref: reference });
}
await sendDisbursementNotification(borrower.phone, loan.principal / 100, loan.term_weeks);
}
Repayment Collection and Overdue Handling
// Send payment link for each installment due
async function sendRepaymentLink(borrowerEmail, repaymentId) {
var repayment = await db.repayments.findById(repaymentId);
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: borrowerEmail,
amount: repayment.amount_due,
currency: 'NGN',
reference: repayment.paystack_ref,
metadata: { repayment_id: repaymentId, loan_id: repayment.loan_id, installment: repayment.installment_number },
}),
});
return (await res.json()).data.authorization_url;
}
// Webhook: record repayment
if (event.event === 'charge.success') {
var meta = event.data.metadata;
if (!meta.repayment_id) return res.sendStatus(200);
await db.repayments.update(meta.repayment_id, {
status: 'paid',
amount_paid: event.data.amount,
paid_at: new Date(),
});
// Check if loan is fully repaid
var pendingCount = await db.repayments.countPending(meta.loan_id);
if (pendingCount === 0) {
await db.loans.update(meta.loan_id, { status: 'completed' });
await notifyBorrowerLoanComplete(meta.loan_id);
}
}
// Cron: flag overdue repayments and apply late penalty
async function processOverdueRepayments() {
var graceDeadline = new Date();
graceDeadline.setDate(graceDeadline.getDate() - 3); // 3-day grace
var overdue = await db.repayments.findAll({ status: 'pending', due_date_lt: graceDeadline });
var LATE_PENALTY_RATE = 0.02; // 2% of installment per week overdue
for (var repayment of overdue) {
var weeksOverdue = Math.floor((new Date() - new Date(repayment.due_date)) / (7 * 24 * 60 * 60 * 1000));
var penalty = Math.floor(repayment.amount_due * LATE_PENALTY_RATE * weeksOverdue);
await db.repayments.update(repayment.id, { status: 'overdue', late_penalty: penalty });
await notifyBorrowerOverdue(repayment.loan_id, repayment.installment_number, penalty / 100);
}
}
Learn More
See build a savings goal app with scheduled debits for card tokenization auto-debit — the same pattern used for recurring loan repayments.
Key Takeaways
- ✓Disburse loans via Paystack Transfers — borrowers receive funds directly to their bank or M-Pesa.
- ✓Generate a repayment schedule at disbursement time: amount, due date, and balance per installment.
- ✓Use card tokenization (first repayment) to auto-debit subsequent installments without manual action.
- ✓Track overdue installments and apply penalty rates after a 3-day grace period.
- ✓Aggregate collection stats per loan officer or group for portfolio health monitoring.
Frequently Asked Questions
- Do I need a lending licence to operate a microfinance repayment portal?
- A repayment collection portal that processes payments on behalf of a licensed microfinance institution (MFI) is a technology service, not a lending activity. The MFI holds the lending licence and takes the credit risk. Your platform facilitates collection. If you are also underwriting and managing the loan risk yourself, you need a money lending licence from CBN (Nigeria) or CBK (Kenya).
- How do I handle group loans (solidarity groups)?
- For group loans, link multiple borrowers to a single loan. Each group member shares equal liability. Send one repayment link to the group leader who collects from members and makes a single payment. Alternatively, split the loan into sub-loans per member and track each member's repayment individually. The group model simplifies collection but complicates defaults — one member's default affects the whole group.
- Can I use card auto-debit for repayments instead of sending payment links?
- Yes. At the first repayment, use a standard Paystack checkout. Save the authorization_code from the charge.success webhook. For subsequent installments, use POST /charge/authorization with the saved code — no need for the borrower to take any action. Send an SMS notification before each auto-debit so the borrower knows to maintain sufficient funds. If a debit fails, send the manual payment link as a fallback.
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