Build a Gift Card System on Paystack
Build a gift card system by: 1) letting customers purchase gift cards via Paystack (any amount), 2) generating a unique redemption code after payment webhook fires, 3) emailing the code to the recipient, 4) allowing code redemption at checkout as a partial or full payment discount, and 5) tracking remaining balance per code.
Gift Card Purchase and Code Generation
Tables: gift_cards (code, initial_amount, remaining_balance, purchased_by, recipient_email, purchased_at, expires_at, status), gift_card_redemptions (gift_card_id, order_id, amount_used, redeemed_at).
var crypto = require('crypto');
function generateGiftCardCode() {
// Format: XXXX-XXXX-XXXX-XXXX (16 uppercase letters/numbers)
var bytes = crypto.randomBytes(8);
var hex = bytes.toString('hex').toUpperCase();
return hex.slice(0, 4) + '-' + hex.slice(4, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16);
}
async function purchaseGiftCard(buyerEmail, recipientEmail, amountInKobo) {
var reference = 'GIFT-' + Date.now();
var tempCode = generateGiftCardCode(); // hold before payment confirms
// Initialize Paystack transaction
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: buyerEmail, amount: amountInKobo, currency: 'NGN', reference,
metadata: { purpose: 'gift_card', recipient_email: recipientEmail, temp_code: tempCode },
}),
});
return (await res.json()).data.authorization_url;
}
// Webhook: create gift card after payment
if (event.event === 'charge.success' && event.data.metadata.purpose === 'gift_card') {
var meta = event.data.metadata;
var expires = new Date();
expires.setFullYear(expires.getFullYear() + 1); // 1-year expiry
await db.giftCards.create({
code: meta.temp_code,
initial_amount: event.data.amount,
remaining_balance: event.data.amount,
purchased_by: event.data.customer.email,
recipient_email: meta.recipient_email,
expires_at: expires,
status: 'active',
});
await sendGiftCardEmail(meta.recipient_email, meta.temp_code, event.data.amount);
}
Redemption at Checkout
async function applyGiftCard(cartTotal, giftCardCode) {
var giftCard = await db.giftCards.findByCode(giftCardCode);
if (!giftCard) throw new Error('Invalid gift card code');
if (giftCard.status !== 'active') throw new Error('Gift card is not active');
if (new Date() > giftCard.expires_at) throw new Error('Gift card has expired');
if (giftCard.remaining_balance <= 0) throw new Error('Gift card has no remaining balance');
var discount = Math.min(giftCard.remaining_balance, cartTotal);
var remainingToPay = cartTotal - discount;
return { discount, remainingToPay, giftCardBalance: giftCard.remaining_balance };
}
async function checkoutWithGiftCard(customerEmail, cartTotal, giftCardCode, orderId) {
var { discount, remainingToPay } = await applyGiftCard(cartTotal, giftCardCode);
// Record the redemption
await db.giftCardRedemptions.create({ gift_card_code: giftCardCode, order_id: orderId, amount_used: discount });
await db.giftCards.decrement(giftCardCode, 'remaining_balance', discount);
if ((await db.giftCards.findByCode(giftCardCode)).remaining_balance === 0) {
await db.giftCards.update(giftCardCode, { status: 'exhausted' });
}
if (remainingToPay <= 0) {
// Fully covered by gift card — complete order without Paystack transaction
await db.orders.update(orderId, { status: 'paid', payment_method: 'gift_card' });
return { fullyPaid: true };
}
// Charge the remaining amount via Paystack
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: { order_id: orderId } }),
});
return { fullyPaid: false, authorizationUrl: (await res.json()).data.authorization_url };
}
Learn More
See build a loyalty and points system tied to payments for a similar value-credit system with a different accumulation model.
Key Takeaways
- ✓Gift card codes should be randomly generated and cryptographically unpredictable — not sequential numbers.
- ✓Track balances per code, not per user — gift cards can be transferred or gifted to anyone.
- ✓Support partial redemption: a NGN 10,000 gift card can be used across multiple orders.
- ✓At checkout, apply the gift card discount server-side before initializing the Paystack transaction.
- ✓Gift cards are a liability until redeemed — account for them in your financial reconciliation.
Frequently Asked Questions
- How do I handle gift cards in financial reconciliation?
- Gift card purchases are a liability, not revenue — you have received money but not yet delivered value. When the gift card is redeemed, you convert the liability to revenue. Maintain a gift_card_liability ledger: credit on purchase, debit on redemption. Report unredeemed gift card balances as liabilities on your balance sheet.
- Can I make gift cards available in different denominations?
- Yes. Either let customers choose a preset amount (NGN 5,000, 10,000, 20,000) or enter a custom amount within a range you define (minimum NGN 1,000, maximum NGN 100,000). Custom amounts make gift cards more flexible; preset amounts simplify your product catalog.
- What if someone enters a wrong gift card code at checkout?
- Return a clear error: "Invalid gift card code. Please check the code and try again." After 3 failed attempts with different codes from the same IP or session, add a brief delay or CAPTCHA to prevent brute-force guessing. Log failed attempts for security monitoring.
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