Resolve Account Number with Paystack
Call GET https://api.paystack.co/bank/resolve with the account_number and bank_code parameters, plus your secret key in the Authorization header. On success, you get back the account_name (the name the bank has on file), the account_number, and the bank_id. Use this to verify a user owns the bank account they entered before creating a transfer recipient or initiating a payout.
What Resolve Account Number Does
When a user enters their bank details in your application, you have no way to know whether they typed the right account number. A single wrong digit sends money to the wrong person. The Resolve Account Number endpoint eliminates this risk.
You send Paystack a bank code and an account number. Paystack asks the bank: "What name is on this account?" The bank responds with the registered account holder name. You display that name to the user and ask them to confirm. If the name matches, you proceed. If it does not, the user can correct their input.
This single call prevents the most common payout error in African fintech: sending money to the wrong bank account because a user mistyped a digit.
The endpoint is GET https://api.paystack.co/bank/resolve. It takes two query parameters: account_number and bank_code. It returns a JSON object with account_number, account_name, and bank_id.
Fetching Bank Codes First
Before you can resolve an account, you need the bank code. This is not the bank's name or SWIFT code. It is a numeric code Paystack assigns to each bank. You get the full list from the /bank endpoint.
// fetch-banks.js
const axios = require('axios');
async function fetchBanks(country) {
const response = await axios.get(
'https://api.paystack.co/bank',
{
params: { country: country }, // 'nigeria', 'kenya', 'ghana', 'south_africa'
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
);
return response.data.data;
}
// Returns an array like:
// [
// { name: "Access Bank", slug: "access-bank", code: "044", ... },
// { name: "GTBank", slug: "guaranty-trust-bank", code: "058", ... },
// ...
// ]
Cache this list. The bank list changes rarely. Fetch it once, store it in your database or a JSON file, and refresh it weekly or monthly. There is no reason to call /bank on every user request.
Build a dropdown. In your UI, show users a dropdown of bank names populated from this cached list. When they select a bank, you have the code you need for the resolve call. Never ask users to type their bank code manually.
Country matters. Pass the country parameter to get banks for a specific country. If you operate in multiple countries, fetch and cache the list for each one.
Calling the Resolve Endpoint
With the bank code in hand, the resolve call is straightforward:
// resolve-account.js
const axios = require('axios');
async function resolveAccountNumber(accountNumber, bankCode) {
const 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,
},
}
);
return response.data.data;
}
// Usage
const result = await resolveAccountNumber('0001234567', '058');
console.log(result.account_name); // "OKAFOR JOHN DOE"
console.log(result.account_number); // "0001234567"
console.log(result.bank_id); // 9
The response shape. On success, the data object contains:
account_number: the account number you sent, echoed back.account_name: the name the bank has on file for this account. This is the critical piece.bank_id: Paystack's internal ID for the bank. You can use this to correlate with the bank list, but you usually do not need it.
Name format. Banks return names in their own format. Some return "SURNAME FIRSTNAME MIDDLENAME". Others return "FIRSTNAME SURNAME". Some include middle names, some do not. Some use all caps, some use mixed case. You cannot assume any particular format.
This is why you need fuzzy name matching, not exact string comparison. The name matching guide covers this in depth.
Displaying the Result to the User
After resolving the account, show the name to the user and ask them to confirm. This is a UX decision that prevents disputes and chargebacks.
A good flow looks like this:
- User selects their bank from a dropdown.
- User types their account number.
- Your frontend sends both values to your backend.
- Your backend calls Resolve Account Number.
- Your backend returns the resolved name to the frontend.
- The frontend displays: "Account holder: OKAFOR JOHN DOE. Is this correct?"
- User clicks "Yes" or "No, let me re-enter."
Format the name. The raw response often comes in all caps. You can title-case it for display: "Okafor John Doe" instead of "OKAFOR JOHN DOE". But store the raw version in your database for matching purposes.
// format-name.js
function titleCase(name) {
return name
.toLowerCase()
.split(' ')
.map(function(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join(' ');
}
// "OKAFOR JOHN DOE" becomes "Okafor John Doe"
Do not skip the confirmation step. Even if you run automated name matching in the background, always show the resolved name to the user. This creates a clear audit trail: the user saw the name and confirmed it was theirs. If they later claim the payout went to the wrong account, you have evidence they confirmed the details.
Handling Errors
The resolve endpoint can fail in several ways. Handle each one explicitly.
422: Could not resolve account name. The account number does not exist at that bank. This is the most common error. The user either mistyped the number or selected the wrong bank. Show a clear message: "We could not find an account with that number at [bank name]. Please check and try again."
429: Rate limit exceeded. You are calling too fast. Back off and retry after a delay. Better yet, use caching to reduce the number of calls.
401: Unauthorized. Your secret key is wrong or missing. Check your environment variables.
5xx: Server error. Paystack or the upstream bank is experiencing issues. Retry with exponential backoff.
// safe-resolve.js
const axios = require('axios');
async function safeResolveAccount(accountNumber, bankCode) {
try {
const 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 { success: true, data: response.data.data };
} catch (error) {
if (error.response) {
var status = error.response.status;
if (status === 422) {
return { success: false, code: 'NOT_FOUND', message: 'Account not found at this bank.' };
}
if (status === 429) {
return { success: false, code: 'RATE_LIMITED', message: 'Please try again in a moment.' };
}
return { success: false, code: 'API_ERROR', message: 'Verification temporarily unavailable.' };
}
if (error.code === 'ECONNABORTED') {
return { success: false, code: 'TIMEOUT', message: 'Bank verification timed out. Please retry.' };
}
return { success: false, code: 'NETWORK', message: 'Could not reach verification service.' };
}
}
Timeouts. Set a timeout on your HTTP client. Bank systems in Africa can be slow. A 15-second timeout is reasonable. If the call times out, tell the user verification is temporarily unavailable and offer to retry.
Caching Resolved Account Names
Account holder names do not change often. Once you resolve an account number, store the result in your database. On subsequent lookups for the same account, check your cache first.
// cached-resolve.js
async function resolveWithCache(accountNumber, bankCode) {
// Check database cache
var cached = await db.query(
'SELECT account_name, resolved_at FROM account_resolutions ' +
'WHERE account_number = $1 AND bank_code = $2 ' +
'AND resolved_at > NOW() - INTERVAL '30 days'',
[accountNumber, bankCode]
);
if (cached.rows.length > 0) {
return {
account_name: cached.rows[0].account_name,
fromCache: true,
};
}
// Call Paystack
var result = await resolveAccountNumber(accountNumber, bankCode);
// Store in cache
await db.query(
'INSERT INTO account_resolutions (account_number, bank_code, account_name, resolved_at) ' +
'VALUES ($1, $2, $3, NOW()) ' +
'ON CONFLICT (account_number, bank_code) ' +
'DO UPDATE SET account_name = $3, resolved_at = NOW()',
[accountNumber, bankCode, result.account_name]
);
return { account_name: result.account_name, fromCache: false };
}
How long to cache. 30 days is a reasonable default. Account names change when someone gets married, legally changes their name, or opens a new account. These events are rare enough that a 30-day cache window is safe for most applications.
Invalidation. Let users trigger a re-resolve manually. If a user says "my bank name has changed," provide a button that bypasses the cache and calls Paystack directly.
Database schema for the cache.
-- account_resolutions table
CREATE TABLE account_resolutions (
id SERIAL PRIMARY KEY,
account_number TEXT NOT NULL,
bank_code TEXT NOT NULL,
account_name TEXT NOT NULL,
resolved_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(account_number, bank_code)
);
Full Express Endpoint Example
Here is a complete Express route that ties everything together: validates input, checks the cache, calls Paystack, and returns a clean response.
// routes/resolve-account.js
const express = require('express');
const router = express.Router();
router.post('/api/resolve-account', async function(req, res) {
var accountNumber = req.body.account_number;
var bankCode = req.body.bank_code;
// Validate input
if (!accountNumber || !bankCode) {
return res.status(400).json({
error: 'Both account_number and bank_code are required.',
});
}
// Strip spaces and dashes
accountNumber = accountNumber.replace(/[\s-]/g, '');
// Basic format check
if (!/^\d{10}$/.test(accountNumber)) {
return res.status(400).json({
error: 'Account number must be exactly 10 digits.',
});
}
// Resolve with caching
var result = await resolveWithCache(accountNumber, bankCode);
if (result.error) {
return res.status(result.statusCode || 500).json({
error: result.message,
});
}
return res.json({
account_name: result.account_name,
account_number: accountNumber,
from_cache: result.fromCache,
});
});
module.exports = router;
Input validation matters. Nigerian bank accounts are 10 digits. Kenyan accounts vary in length depending on the bank (some are 10 digits, others are 13 or 14). Strip spaces and dashes before sending to Paystack. A user who types "000-123-4567" should not get an error.
Rate-limit your own endpoint. Even though Paystack rate-limits on their end, add rate limiting to your own resolve endpoint. A malicious user could spam it to enumerate valid account numbers. Use a library like express-rate-limit to cap requests per IP.
Testing with Test Keys
When you use test secret keys, Paystack returns mock data. The resolved account name will be something like "Test Account" rather than a real name. This is fine for verifying your integration logic, but it will not catch issues with real bank data formats.
Test the error paths. Send invalid account numbers to confirm your error handling works. Send requests with no secret key to verify you get a 401. Send many requests quickly to see how your rate-limit handling behaves.
Move to live keys carefully. When you switch to live keys, test with your own bank account first. Confirm the resolved name matches what your bank has on file. Then test with a colleague's account. Only then open the feature to users.
For the full picture of how account resolution fits into identity verification, read the complete identity verification guide.
Key Takeaways
- ✓Resolve Account Number takes a bank code and account number and returns the account holder name. It is the most important identity call in a payout flow.
- ✓Fetch bank codes from GET /bank with the country parameter. Cache the list locally because it changes rarely.
- ✓The returned name is in whatever format the bank stores it, often uppercase and surname-first. You need fuzzy matching to compare it with user-entered names.
- ✓A 422 response means the account does not exist at that bank. This is not a bug. Show the user a clear message.
- ✓Cache resolved account names for at least 30 days. Account holder names change rarely, and the endpoint is rate-limited.
- ✓Always call this from your backend. The endpoint requires your secret key.
Frequently Asked Questions
- What bank code do I use for Resolve Account Number?
- Fetch the bank code from the GET /bank endpoint with the country parameter. Each bank has a numeric code assigned by Paystack. For example, GTBank is "058" in Nigeria. Cache the list because it changes rarely.
- Why does the resolved name not match what the user typed?
- Banks store names in their own format. Some use SURNAME FIRSTNAME, others use FIRSTNAME SURNAME. Some include middle names, some do not. You need fuzzy matching to compare the resolved name with the name the user entered during registration.
- Can I resolve Kenyan bank accounts with Paystack?
- Yes. Pass country: "kenya" when fetching bank codes from the /bank endpoint, then use the returned bank codes with the resolve endpoint. The flow is the same as for Nigerian accounts.
- How do I handle a 422 response from Resolve Account Number?
- A 422 means the account number does not exist at that bank. The user likely mistyped the number or selected the wrong bank. Show a clear error message and let them re-enter their details. This is not a bug in your code.
- Is there a cost per Resolve Account Number call?
- Check your Paystack dashboard or contact your account manager for current pricing. The cost structure may vary by account type and volume. Regardless, caching results reduces unnecessary API calls and keeps costs predictable.
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