Validating a Customer Before Payout
Before initiating a Paystack transfer, resolve the recipient bank account to confirm it exists and get the registered name. Compare that name against what the user provided using fuzzy matching. For high-value payouts, verify BVN through the Validate Customer endpoint. Enforce daily and per-transaction limits. Only create a transfer recipient and initiate the transfer after all checks pass.
Why Pre-Payout Validation Matters
A payout is a push transaction. Once the money leaves your Paystack balance and reaches the recipient's bank account, getting it back requires the recipient's cooperation. Banks can attempt a reversal, but there is no guarantee.
The most common payout mistakes are:
- Wrong account number. A user mistypes one digit. The money goes to a stranger. You now need that stranger to voluntarily return it.
- Social engineering. A fraudster convinces your support team to change payout details. The next payout goes to the fraudster's account.
- Account takeover. Someone gains access to a user's account on your platform and changes the payout bank details to their own.
- Duplicate payouts. A bug or retry logic sends the same payout twice.
Pre-payout validation catches all of these. It adds a few seconds to the payout process and saves you from losses that can be difficult or impossible to reverse.
Step 1: Resolve the Account Number
The first check is always account resolution. Before you do anything else, confirm that the bank account exists and get the name on it.
// validate-payout.js
const axios = require('axios');
async function resolveAccount(accountNumber, bankCode) {
var response = await axios.get(
'https://api.paystack.co/bank/resolve',
{
params: {
account_number: accountNumber,
bank_code: bankCode,
},
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
timeout: 15000,
}
);
return response.data.data;
}
If resolution fails (422): The account does not exist at that bank. Stop immediately. Do not proceed with the payout. Tell the user their bank details could not be verified and ask them to re-enter.
If resolution times out: Do not proceed. African bank systems can be slow, but an unresolved account is an unverified account. Retry after a short delay. If it times out three times, tell the user to try again later.
For detailed error handling on this endpoint, see the Resolve Account Number guide.
Step 2: Match the Name
You now have the bank's registered account name. Compare it with the name your user provided during registration or payout setup.
This comparison is harder than it sounds. Banks format names differently:
- One bank returns "OKAFOR JOHN DOE"
- Another returns "John Doe Okafor"
- Another returns "JOHN D. OKAFOR"
Exact string comparison will reject all of these as mismatches even when they refer to the same person.
// name-match.js
function normalizeNameParts(name) {
return name
.toUpperCase()
.replace(/[^A-Z\s]/g, '')
.split(/\s+/)
.filter(function(part) { return part.length > 1; })
.sort();
}
function namesMatch(name1, name2) {
var parts1 = normalizeNameParts(name1);
var parts2 = normalizeNameParts(name2);
// Count how many name parts overlap
var matches = 0;
parts1.forEach(function(part) {
if (parts2.indexOf(part) !== -1) {
matches++;
}
});
// Require at least 2 matching parts (first name + last name)
var minParts = Math.min(parts1.length, parts2.length);
return matches >= Math.min(2, minParts);
}
What to do on mismatch: Show the user the resolved name and ask: "The bank account is registered to [name]. Is this your account?" Let them confirm or re-enter. Do not automatically reject the payout. Women who recently married, people with multiple legal names, and people who use shortened versions of their name are all legitimate.
For a deeper treatment of name matching, see the fuzzy comparison guide.
Step 3: Enforce Payout Limits
Even if the account resolves and the name matches, you should enforce limits before sending money.
// payout-limits.js
async function checkPayoutLimits(userId, amount) {
var errors = [];
// Per-transaction limit
var maxPerTransaction = 500000 * 100; // 500,000 NGN in kobo
if (amount > maxPerTransaction) {
errors.push('Amount exceeds per-transaction limit');
}
// Daily limit
var todayTotal = await db.query(
'SELECT COALESCE(SUM(amount), 0) as total FROM payouts ' +
'WHERE user_id = $1 AND status = $2 AND created_at > NOW() - INTERVAL '24 hours'',
[userId, 'success']
);
var dailyLimit = 2000000 * 100; // 2,000,000 NGN in kobo
if (todayTotal.rows[0].total + amount > dailyLimit) {
errors.push('Would exceed daily payout limit');
}
// Velocity check: max 5 payouts per hour
var recentCount = await db.query(
'SELECT COUNT(*) as count FROM payouts ' +
'WHERE user_id = $1 AND created_at > NOW() - INTERVAL '1 hour'',
[userId]
);
if (parseInt(recentCount.rows[0].count) >= 5) {
errors.push('Too many payouts in the last hour');
}
return { allowed: errors.length === 0, errors: errors };
}
Set limits based on your risk appetite. A payroll product sending salaries once a month has different limits than a marketplace sending daily settlements. Start conservative and increase limits as you gain confidence in your users.
Tiered limits. New users get lower limits. Users who have completed BVN verification get higher limits. Users with a long transaction history get the highest limits. This rewards trust without exposing you to risk from unverified accounts.
Step 4: BVN Verification for High-Value Payouts
For payouts above a threshold you define, require BVN verification before proceeding. This adds a second layer of identity confirmation beyond account resolution.
// high-value-check.js
var HIGH_VALUE_THRESHOLD = 100000 * 100; // 100,000 NGN in kobo
async function requiresEnhancedVerification(userId, amount) {
if (amount < HIGH_VALUE_THRESHOLD) {
return false;
}
// Check if user is already BVN-verified
var verification = await db.query(
'SELECT verified FROM identity_verifications ' +
'WHERE user_id = $1 AND verification_type = $2 AND verified = true',
[userId, 'bvn']
);
if (verification.rows.length > 0) {
return false; // Already verified, no need to re-verify
}
return true; // Needs BVN verification before this payout
}
When to trigger BVN verification: Before the first high-value payout, not during it. If a user is setting up their payout details, run BVN verification as part of the setup. Do not wait until they request a large payout. That creates friction at the worst moment.
For full BVN verification implementation, see the BVN verification guide.
Step 5: Create the Transfer Recipient
Only after all validation passes should you create a transfer recipient in Paystack.
// create-recipient.js
async function createTransferRecipient(name, accountNumber, bankCode) {
var response = await axios.post(
'https://api.paystack.co/transferrecipient',
{
type: 'nuban',
name: name,
account_number: accountNumber,
bank_code: bankCode,
currency: 'NGN',
},
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
}
);
return response.data.data;
}
Store the recipient code. Save the recipient_code in your database linked to the user. For future payouts to the same account, reuse the recipient code instead of creating a new one.
Re-validate on changes. If a user changes their bank details, do not just update the existing recipient. Delete the old one, run the full validation again (resolve, name match, limits), and create a new recipient only after everything passes.
The First-Payout Hold Period
Consider holding the first payout to a new bank account for 24 hours before releasing it. This gives you time to catch fraud that automated checks miss.
How it works:
- User requests a payout to a new bank account.
- All automated checks pass (resolution, name match, limits).
- You create the payout record with status "pending_review" instead of initiating it immediately.
- After 24 hours, a background job checks if the user's account shows any suspicious activity (login from new IP, sudden profile changes, customer complaints).
- If everything looks clean, initiate the transfer.
Communicate clearly. Tell the user upfront: "First payouts to new bank accounts take up to 24 hours for security verification. Subsequent payouts to the same account process immediately." Users accept this when they understand it protects their money.
Skip the hold for verified users. If a user has completed BVN verification and has a history of successful payouts, skip the hold period. Reward trust.
Building an Audit Trail
Log every step of the validation process. When a dispute arises weeks later, your logs are your evidence.
// audit-log.js
async function logValidationStep(payoutId, step, result, details) {
await db.query(
'INSERT INTO payout_audit_log (payout_id, step, result, details, created_at) ' +
'VALUES ($1, $2, $3, $4, NOW())',
[payoutId, step, result, JSON.stringify(details)]
);
}
// Usage throughout the validation flow
await logValidationStep(payoutId, 'account_resolution', 'success', {
account_name: 'OKAFOR JOHN DOE',
bank_code: '058',
});
await logValidationStep(payoutId, 'name_match', 'pass', {
user_name: 'John Okafor',
resolved_name: 'OKAFOR JOHN DOE',
match_score: 0.85,
});
await logValidationStep(payoutId, 'limit_check', 'pass', {
amount: 5000000,
daily_total: 12000000,
daily_limit: 200000000,
});
What to log: The resolved account name, the name comparison result (including both names and the match score), the limit check results, whether BVN verification was required and what the outcome was, and the final decision (approved or rejected).
Retention. Keep payout audit logs for at least 2 years. Regulatory requirements in Nigeria and Kenya may require longer. Check with your legal team.
Key Takeaways
- ✓Always resolve the account number before creating a transfer recipient. This confirms the account exists and gives you the registered name.
- ✓Use fuzzy name matching, not exact string comparison, to compare the resolved name with the user-provided name. Banks format names differently.
- ✓Enforce payout limits at the application level. Set per-transaction maximums, daily ceilings, and velocity limits that match your risk appetite.
- ✓For high-value payouts, require BVN verification in addition to account resolution. Two verification points are stronger than one.
- ✓Log every validation step with timestamps. When disputes arise, your logs prove you did due diligence before sending the money.
- ✓Build a hold period for first-time recipients. The first payout to a new bank account can wait 24 hours. Fraud rarely survives a cooling-off period.
Frequently Asked Questions
- Can I skip account resolution and just create a transfer recipient directly?
- Technically yes, but you should not. Creating a transfer recipient without resolving the account first means you have no idea if the account exists or who owns it. If the account number is wrong, the transfer will fail. If it is right but belongs to someone else, you just sent money to a stranger.
- How do I handle a user who changes their bank details frequently?
- Frequent bank detail changes are a fraud signal. Implement a cooldown period: after changing bank details, the user cannot request a payout for 24 to 48 hours. Run the full validation flow each time details change. If changes happen more than twice in a month, flag the account for manual review.
- What payout limits should I start with?
- Start conservative. A reasonable starting point for a new product is a per-transaction limit that matches your typical transaction size and a daily limit that is 3 to 5 times that. Increase limits as you gain confidence in your fraud detection and user verification. Every business is different, so adjust based on your actual risk experience.
- Should I validate the recipient every time or just the first time?
- Resolve the account at least on the first payout to a new bank account and whenever the user changes their bank details. For repeat payouts to the same verified account, you can skip resolution and reuse the stored transfer recipient code. Re-validate periodically, perhaps monthly, to catch accounts that have been closed.
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