Build an invoicing tool by creating invoice records, generating a Paystack transaction link for each invoice, and marking the invoice as paid when the charge.success webhook fires with the matching reference. A daily cron job checks overdue invoices and sends reminder emails at defined intervals.
Tables: invoices (issuer_id, client_name, client_email, items[], subtotal, tax, total, currency, due_date, status, paystack_ref, paid_at), reminder_log (invoice_id, reminder_type, sent_at).
async function createInvoice(issuerId, clientEmail, clientName, items, dueDate) {
var subtotal = items.reduce(function(sum, item) { return sum + (item.qty * item.unit_price); }, 0);
var tax = Math.floor(subtotal * 0.075); // 7.5% VAT (Nigeria)
var total = subtotal + tax;
var reference = 'INV-' + Date.now();
var invoice = await db.invoices.create({
issuer_id: issuerId,
client_name: clientName,
client_email: clientEmail,
items,
subtotal,
tax,
total,
due_date: dueDate,
status: 'sent',
paystack_ref: reference,
});
// Generate Paystack payment link
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: clientEmail,
amount: total,
currency: 'NGN',
reference,
metadata: { invoice_id: invoice.id, issuer_id: issuerId },
}),
});
var paymentUrl = (await res.json()).data.authorization_url;
await sendInvoiceEmail(clientEmail, clientName, invoice, paymentUrl);
return { invoice, paymentUrl };
}
// Webhook: mark invoice paid
if (event.event === 'charge.success') {
var meta = event.data.metadata;
if (meta.invoice_id) {
await db.invoices.update(meta.invoice_id, { status: 'paid', paid_at: new Date() });
await generateAndSendReceipt(meta.invoice_id, event.data.reference);
}
}
// Cron: runs daily to detect overdue invoices and send reminders
async function processOverdueInvoices() {
var today = new Date();
var pendingInvoices = await db.invoices.findAll({ status: 'sent' });
for (var invoice of pendingInvoices) {
var daysOverdue = Math.floor((today - new Date(invoice.due_date)) / (1000 * 60 * 60 * 24));
if (daysOverdue <= 0) continue; // not yet overdue
var remindersAlreadySent = await db.reminderLog.findByInvoice(invoice.id);
var sentTypes = remindersAlreadySent.map(function(r) { return r.reminder_type; });
var toSend = null;
if (daysOverdue >= 3 && !sentTypes.includes('3day')) toSend = '3day';
if (daysOverdue >= 7 && !sentTypes.includes('7day')) toSend = '7day';
if (daysOverdue >= 14 && !sentTypes.includes('14day')) toSend = '14day';
if (toSend) {
// Re-initialize payment link (same reference re-usable until paid)
var paymentUrl = await getPaymentUrl(invoice.paystack_ref, invoice.client_email, invoice.total);
await sendReminderEmail(invoice.client_email, invoice.client_name, invoice, paymentUrl, toSend);
await db.reminderLog.create({ invoice_id: invoice.id, reminder_type: toSend, sent_at: new Date() });
if (toSend === '14day') {
await db.invoices.update(invoice.id, { status: 'overdue' });
await notifyIssuer(invoice.issuer_id, 'Invoice #' + invoice.id + ' is 14 days overdue.');
}
}
}
}
// Mark invoice as void if client and issuer agree it should not be paid
async function voidInvoice(invoiceId) {
var invoice = await db.invoices.findById(invoiceId);
if (invoice.status === 'paid') throw new Error('Cannot void a paid invoice');
await db.invoices.update(invoiceId, { status: 'void' });
}
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 freelance escrow style platform for a related tool that holds payment until work is approved.
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