Build a Savings Goal App with Scheduled Debits
Build a savings goal app by collecting the user's first deposit via Paystack (which tokenizes their card), saving the authorization_code, and running scheduled debits using Charge Authorization on a cron job. When the goal is reached, pay out via Paystack Transfers. The user never has to re-enter their card.
Goal Setup and Card Tokenization
Tables: savings_goals (user_id, name, target_amount, saved_amount, frequency, next_debit_date, authorization_code, status), savings_transactions (goal_id, type, amount, paystack_ref, status, created_at).
// Step 1: First deposit — tokenizes the card
async function createGoalAndFirstDeposit(userId, userEmail, goalName, targetAmount, firstDepositAmount, frequency) {
var goal = await db.savingsGoals.create({
user_id: userId, name: goalName, target_amount: targetAmount,
saved_amount: 0, frequency, status: 'pending_first_deposit',
});
var reference = 'SAVE-' + goal.id + '-' + Date.now();
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: userEmail, amount: firstDepositAmount, currency: 'NGN', reference,
metadata: { goal_id: goal.id, user_id: userId, deposit_type: 'first' },
}),
});
return (await res.json()).data.authorization_url;
}
// Webhook: save authorization_code from first deposit
if (event.event === 'charge.success' && event.data.metadata.deposit_type === 'first') {
var goalId = event.data.metadata.goal_id;
var authCode = event.data.authorization.authorization_code;
var nextDebit = getNextDebitDate(new Date(), goal.frequency); // 'weekly' or 'monthly'
await db.savingsGoals.update(goalId, {
authorization_code: authCode,
saved_amount: event.data.amount,
next_debit_date: nextDebit,
status: 'active',
});
await db.savingsTransactions.create({ goal_id: goalId, type: 'deposit', amount: event.data.amount, paystack_ref: event.data.reference, status: 'success' });
}
Scheduled Debits and Goal Completion
// Cron job — runs daily, processes goals due today
async function processScheduledDebits() {
var today = new Date().toISOString().split('T')[0];
var dueGoals = await db.savingsGoals.findDueToday(today);
for (var goal of dueGoals) {
var user = await db.users.findById(goal.user_id);
var debitAmount = goal.debit_amount; // fixed amount per period
try {
var res = await fetch('https://api.paystack.co/charge/authorization', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
email: user.email,
amount: debitAmount,
authorization_code: goal.authorization_code,
metadata: { goal_id: goal.id, user_id: goal.user_id, deposit_type: 'scheduled' },
}),
});
var data = await res.json();
if (data.data.status === 'success') {
var newSaved = goal.saved_amount + debitAmount;
await db.savingsGoals.update(goal.id, {
saved_amount: newSaved,
next_debit_date: getNextDebitDate(new Date(), goal.frequency),
});
if (newSaved >= goal.target_amount) {
await db.savingsGoals.update(goal.id, { status: 'goal_reached' });
await notifyGoalReached(goal.user_id, goal);
}
} else {
await db.savingsTransactions.create({ goal_id: goal.id, type: 'deposit', amount: debitAmount, status: 'failed' });
await sendDebitFailedNotification(user, goal);
}
} catch (err) {
console.error('Debit failed for goal', goal.id, err);
}
}
}
// User requests withdrawal after goal is reached
async function withdrawSavings(goalId, userId) {
var goal = await db.savingsGoals.findById(goalId);
if (goal.status !== 'goal_reached') throw new Error('Goal not yet reached');
var user = await db.users.findById(userId);
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: goal.saved_amount, recipient: user.paystack_recipient_code, reason: 'Savings goal: ' + goal.name }),
});
await db.savingsGoals.update(goalId, { status: 'withdrawn' });
}
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 water delivery subscription service for a related recurring charge pattern.
Key Takeaways
- ✓Use card tokenization (authorization_code from the first charge) for all subsequent scheduled debits.
- ✓Run scheduled debits via a cron job using the Paystack Charge Authorization endpoint.
- ✓Handle debit failures gracefully: retry once, then notify the user to update their card.
- ✓Pay out savings via Paystack Transfers when the goal is met — to bank account or M-Pesa.
- ✓A savings product holds user funds — ensure you have appropriate licensing if you hold funds overnight.
Frequently Asked Questions
- Is holding customer savings funds legal without a licence in Nigeria or Kenya?
- Holding customer funds overnight (acting as a store of value) generally requires a financial services licence — a Payment Service Bank (PSB) licence in Nigeria or an e-money licence in Kenya. A simpler approach for early-stage products: use a regulated custodian partner (a licensed bank or fintech) to hold funds while your app handles the UX. Consult a financial regulatory lawyer before launching a savings product.
- What happens if the user's card expires between debits?
- The charge will fail with an expired card error. Send the user a notification to update their payment method. Provide a re-authorization link (new Paystack checkout) where they can pay with a new card — this gives you a new authorization_code to replace the old one. Until they update, pause the scheduled debits.
- Can users withdraw early (before reaching the goal)?
- Yes, but make this a deliberate decision with clear consequences. An early withdrawal could include a penalty (e.g., lose 2% of saved amount) or simply cancel the goal with a full withdrawal. State the early withdrawal policy clearly at goal creation. This policy should balance user flexibility with the savings discipline you want to encourage.
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