Build a loyalty points system by listening to the charge.success webhook to award points (e.g., 1 point per NGN 100 spent), maintaining a points balance per customer, and allowing points redemption at checkout by reducing the Paystack transaction amount. Always calculate points on the backend — never trust client-submitted point values.
Tables: loyalty_accounts (customer_id, points_balance, lifetime_points, tier), point_transactions (customer_id, type, points, order_id, paystack_ref, description, expires_at, created_at).
var POINTS_PER_KOBO = 1 / 10000; // 1 point per NGN 100 (10000 kobo)
var POINT_VALUE_IN_KOBO = 500; // 1 point = NGN 5 (500 kobo) when redeemed
// Webhook: award points on successful payment
if (event.event === 'charge.success') {
var customerId = event.data.metadata.customer_id;
if (!customerId) return res.sendStatus(200);
var paidAmount = event.data.amount; // in kobo
var pointsEarned = Math.floor(paidAmount * POINTS_PER_KOBO);
if (pointsEarned > 0) {
var expires = new Date();
expires.setFullYear(expires.getFullYear() + 1);
await db.pointTransactions.create({
customer_id: customerId,
type: 'earn',
points: pointsEarned,
paystack_ref: event.data.reference,
description: 'Points earned on purchase',
expires_at: expires,
});
await db.loyaltyAccounts.increment(customerId, { points_balance: pointsEarned, lifetime_points: pointsEarned });
// Update tier based on lifetime spend
var account = await db.loyaltyAccounts.findByCustomer(customerId);
var tier = account.lifetime_points >= 10000 ? 'gold' : account.lifetime_points >= 3000 ? 'silver' : 'bronze';
if (tier !== account.tier) await db.loyaltyAccounts.update(customerId, { tier });
}
}
async function redeemPoints(customerId, cartTotal, pointsToRedeem) {
var account = await db.loyaltyAccounts.findByCustomer(customerId);
if (account.points_balance < pointsToRedeem) throw new Error('Insufficient points');
var discountInKobo = pointsToRedeem * POINT_VALUE_IN_KOBO;
var maxDiscount = Math.floor(cartTotal * 0.3); // max 30% off via points
var actualDiscount = Math.min(discountInKobo, maxDiscount);
var actualPointsUsed = Math.ceil(actualDiscount / POINT_VALUE_IN_KOBO);
var remainingToPay = cartTotal - actualDiscount;
return { actualPointsUsed, actualDiscount, remainingToPay };
}
async function checkoutWithPoints(customerId, customerEmail, cartTotal, pointsToRedeem, orderId) {
var { actualPointsUsed, actualDiscount, remainingToPay } = await redeemPoints(customerId, cartTotal, pointsToRedeem);
// Deduct points immediately
await db.loyaltyAccounts.decrement(customerId, 'points_balance', actualPointsUsed);
await db.pointTransactions.create({
customer_id: customerId, type: 'redeem',
points: -actualPointsUsed, order_id: orderId, description: 'Points redeemed at checkout',
});
if (remainingToPay <= 0) {
await db.orders.update(orderId, { status: 'paid', payment_method: 'points' });
return { fullyPaid: true };
}
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: customerEmail,
amount: remainingToPay,
metadata: { customer_id: customerId, order_id: orderId, points_redeemed: actualPointsUsed },
}),
});
return { fullyPaid: false, authorizationUrl: (await res.json()).data.authorization_url };
}
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 gift card system on Paystack for a complementary stored-value mechanism alongside loyalty points.
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