Build a Loyalty and Points System Tied to Payments
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.
Earning Points on Payments
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 });
}
}
Points Redemption at Checkout
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.
Learn More
See build a gift card system on Paystack for a complementary stored-value mechanism alongside loyalty points.
Key Takeaways
- ✓Award points only after charge.success webhook — never on checkout initiation.
- ✓Calculate points redemption value server-side; never trust the client to report how many points to apply.
- ✓Store point transactions (earned, redeemed) for a complete audit trail and customer balance history.
- ✓Implement point expiry (12 months from earning) to manage liability and encourage spending.
- ✓Support tiered membership (Bronze, Silver, Gold) based on cumulative spend with different earn rates.
Frequently Asked Questions
- What if the customer's Paystack payment fails after I deducted their points?
- Listen for the charge.failed webhook. If a payment fails for an order where points were redeemed, restore the points to the customer's account immediately. Implement this as a compensating transaction in your point_transactions table (type: "restore") and increment the points_balance accordingly.
- How do I handle point expiry?
- Each point_transaction row has an expires_at date. When calculating the available balance, sum only non-expired points. Run a nightly cron job that marks expired points as "expired" and adjusts the loyalty_accounts points_balance accordingly. Notify customers 30 days before their points expire to encourage spending.
- Should I display the points balance on every page or just at checkout?
- Display the points balance prominently in the customer's account header and on the checkout page. A customer who sees "You have 850 points = NGN 4,250" is more likely to make a purchase to use those points. On the product page, consider showing "Earn 25 points with this purchase" to incentivize buying.
Learn Paystack Integration
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