Paystack Transfers and Payouts: Complete Guide
Paystack Transfers let you send money from your Paystack balance to bank accounts and mobile money wallets across Nigeria, Ghana, Kenya, and South Africa. You create a transfer recipient, then initiate a transfer to that recipient. Transfers require OTP confirmation by default, but you can disable OTP for automated systems after meeting Paystack's requirements.
How Paystack Transfers Work End to End
A Paystack Transfer moves money from your Paystack balance to someone else's bank account or mobile money wallet. This is the reverse of accepting payments. When customers pay you, money flows into your Paystack balance. Transfers send it back out.
The flow has three steps:
- Create a transfer recipient. You tell Paystack who you want to pay by providing their bank account number and bank code (or mobile money details). Paystack validates the details, resolves the account name, and returns a
recipient_codeyou will use for all future transfers to that person. - Initiate the transfer. You POST to the transfer endpoint with the recipient code, amount, and a unique reference. Paystack queues the transfer and returns a
transfer_code. - Confirm OTP (if required). By default, Paystack sends an OTP to the business owner's phone or email. You submit the OTP to finalize the transfer. If OTP is disabled on your account, the transfer processes immediately.
After confirmation, Paystack sends the money through the banking network. The transfer moves through several statuses: pending while processing, success when the money lands, or failed if something goes wrong (invalid account, bank downtime, insufficient balance). You receive webhook notifications for each status change.
One thing that catches people: transfers come from your available balance, not your total balance. If you have NGN 500,000 in total balance but NGN 200,000 is pending settlement, you can only transfer from the remaining NGN 300,000. Always check your balance before initiating transfers. For the full lifecycle with diagrams, see how Paystack transfers work end to end.
This guide is part of the complete Paystack engineering guide.
Transfer Recipients: Bank Accounts and Mobile Money
Before you can send money to anyone, you must create a transfer recipient. A recipient stores the destination account details and gives you a reusable recipient_code. You create it once, then use that code for every transfer to that person.
Creating a bank account recipient (Nigeria)
const createRecipient = async (accountNumber, bankCode, name) => {
const response = await fetch('https://api.paystack.co/transferrecipient', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'nuban', // Nigerian bank account
name: name, // Account holder name
account_number: accountNumber,
bank_code: bankCode, // e.g., '058' for GTBank
currency: 'NGN',
}),
});
const data = await response.json();
// data.data.recipient_code => 'RCP_xxxxxxxx'
// data.data.details.account_name => resolved name from bank
return data.data;
};
// Usage
const recipient = await createRecipient('0123456789', '058', 'Amina Okafor');
console.log(recipient.recipient_code); // RCP_abc123def456
The type field changes depending on the destination country and account type:
nubanfor Nigerian bank accountsghipssfor Ghanaian bank accountsmobile_moneyfor mobile money wallets (Ghana)basafor South African bank accountsauthorizationfor charging a saved card authorization (refund-via-transfer pattern)
For Kenya, you create nuban-type recipients for bank accounts using Kenyan bank codes and KES currency. M-Pesa recipients use the mobile money flow with the phone number as the account identifier.
Paystack validates the account details when you create the recipient. If the account number and bank code do not match a real account, the request fails. This is your first line of defense against sending money to the wrong person. Store the resolved account_name from Paystack's response and show it to your user for confirmation before initiating any transfer.
Recipients are permanent. Once created, the recipient_code stays valid until you delete it. You should save recipient codes in your database, tied to the user or vendor they belong to. Do not recreate recipients every time you want to send money. For the full details on recipient types, validation, and updates, see creating transfer recipients on Paystack.
Initiating a Single Transfer
Once you have a recipient code, sending money is a single API call.
const initiateTransfer = async (recipientCode, amountInNaira, reason) => {
const reference = `payout_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const response = 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', // Always 'balance'
amount: amountInNaira * 100, // Convert to kobo
recipient: recipientCode,
reason: reason, // Shows up in recipient's bank statement
reference: reference, // Your unique identifier
}),
});
const data = await response.json();
// data.data.transfer_code => 'TRF_xxxxxxxx'
// data.data.status => 'otp' or 'pending'
return data.data;
};
// Usage
const transfer = await initiateTransfer(
'RCP_abc123def456',
5000, // 5,000 Naira
'Vendor payment - July 2026'
);
Key points about the request:
sourceis always"balance". There is no other valid value.amountis in the smallest currency unit: kobo for NGN, pesewas for GHS, cents for KES and ZAR. If you want to send 5,000 Naira, pass 500000.reasonis optional but recommended. It appears in the recipient's bank statement (truncated by some banks) and in your Paystack dashboard. It makes reconciliation much easier.referenceis your unique identifier for this transfer. If you do not provide one, Paystack generates a random one. Always provide your own for idempotency and tracking.
If OTP is enabled on your account, the transfer status comes back as "otp". Paystack sends a one-time password to the registered phone number or email on your Paystack business account. You must submit it to finalize. If OTP is disabled, the status will be "pending" and processing starts immediately.
After the transfer is initiated, do not treat it as complete. The money has not moved yet. You must listen for the transfer.success or transfer.failed webhook to know the outcome. For complete single transfer patterns including retry logic, see single transfers with the Paystack API.
Bulk Transfers: Paying Multiple Recipients at Once
When you need to pay dozens or hundreds of people at once (vendor payouts, payroll, affiliate commissions), sending transfers one by one is slow and fragile. The bulk transfer endpoint handles up to 100 transfers in a single API call.
const initiateBulkTransfer = async (transfers) => {
const response = await fetch('https://api.paystack.co/transfer/bulk', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
currency: 'NGN',
source: 'balance',
transfers: transfers,
}),
});
const data = await response.json();
return data.data;
};
// Build the batch
const transfers = [
{
amount: 250000, // 2,500 NGN in kobo
recipient: 'RCP_abc123',
reason: 'July payout - Driver A',
reference: 'payout_july_driver_001',
},
{
amount: 180000, // 1,800 NGN in kobo
recipient: 'RCP_def456',
reason: 'July payout - Driver B',
reference: 'payout_july_driver_002',
},
{
amount: 320000, // 3,200 NGN in kobo
recipient: 'RCP_ghi789',
reason: 'July payout - Driver C',
reference: 'payout_july_driver_003',
},
];
const result = await initiateBulkTransfer(transfers);
Each transfer in the batch is independent. If Driver B's bank account is invalid, that transfer fails while Driver A and Driver C still succeed. You receive separate webhook events for each transfer in the batch.
Important constraints for bulk transfers:
- Maximum 100 transfers per batch. If you have more, split them into multiple batches.
- All transfers in a batch must use the same currency.
- Each transfer needs its own unique reference. If two transfers share a reference, one will be treated as a duplicate.
- Your balance must cover the total amount of all transfers in the batch. If your balance is insufficient, the entire batch fails.
For large payout runs (thousands of recipients), you will need to batch your transfers into groups of 100, add delays between batches to stay within rate limits, and track the status of each batch. For production patterns including batching strategies and progress tracking, see bulk transfers with the Paystack API.
The OTP Requirement and How to Automate Around It
By default, every transfer you initiate on Paystack requires OTP confirmation. Paystack sends a code to the phone number or email registered on your business account. You call /transfer/finalize_transfer with the OTP and the transfer_code to release the funds.
This is a security measure. It prevents anyone who gets hold of your secret key from draining your balance. But it also means you cannot run fully automated payouts. Every transfer needs a human to type in a code.
Disabling OTP for automation
Paystack lets you disable OTP so transfers process immediately without manual confirmation. You can do this through the dashboard or via the API. The API flow works like this:
- Call
POST /transfer/disable_otpto request OTP disabling. - Paystack sends a final OTP to your registered contact.
- Call
POST /transfer/disable_otp_finalizewith that OTP. - OTP is now disabled. All future transfers process without confirmation.
Once OTP is off, any valid API call with your secret key can move money out of your account. This is the tradeoff: convenience for security. Before disabling OTP, make sure you have strong controls on your secret key, IP allowlisting if Paystack supports it for your account, and internal approval workflows in your application code.
If you ever need to re-enable OTP, call POST /transfer/enable_otp. It takes effect immediately.
Most production payout systems disable OTP and implement their own approval controls at the application layer: maker-checker flows, admin approval dashboards, or batch review before execution. The goal is to automate the Paystack side while keeping human oversight in your own system. For the full setup process and security recommendations, see transfer OTP: enabling, disabling, and automating.
Transfer Statuses and Failure Handling
Every transfer moves through a set of statuses. Understanding them is essential for building reliable payout systems.
otp: Transfer is waiting for OTP confirmation. Only appears when OTP is enabled.pending: Transfer has been accepted and is being processed. Money has left your Paystack balance but has not reached the recipient yet.success: Money has been delivered to the recipient's account.failed: The transfer could not be completed. Your balance is refunded.reversed: The transfer was initially marked as successful but was later reversed (rare, usually due to bank issues).
The key webhook events to listen for are transfer.success, transfer.failed, and transfer.reversed. Your webhook handler should update your database records based on these events.
Common failure reasons
Transfers fail for predictable reasons:
- Insufficient balance. Your available balance does not cover the transfer amount. Always check your balance before initiating.
- Invalid account. The recipient's account number or bank code is wrong. The account may have been closed or frozen since you created the recipient.
- Bank downtime. The destination bank's systems are temporarily unavailable. This is common in parts of Africa, especially during maintenance windows.
- Transfer limits. The recipient's account has reached daily or transaction limits imposed by their bank or by regulatory requirements.
When a transfer fails, Paystack refunds the amount to your balance. You can retry the transfer with the same recipient (if the failure was temporary, like bank downtime) or ask the user to verify their account details (if the failure was an invalid account).
Never retry automatically without checking the failure reason first. If an account is invalid, retrying will fail every time and just burn through your rate limits. If it was a timeout or bank error, a retry after a short delay often succeeds. For retry patterns and reversal handling, see managing transfer failures and reversals.
M-Pesa, Paybill, Till, and Kenyan Bank Transfers
Kenya's payment landscape is dominated by M-Pesa, so Paystack transfer support here goes beyond standard bank accounts. You can send money to M-Pesa wallets, Paybills (business accounts), Tills (merchant tills), and regular bank accounts.
M-Pesa wallet transfers
To send money to an M-Pesa wallet, create a mobile money transfer recipient using the phone number in international format (254XXXXXXXXX, no leading zero, with Kenya's country code). The recipient receives the money directly in their M-Pesa wallet, and they get the standard M-Pesa SMS notification.
Paybill and Till transfers
Sending to Paybills and Tills follows a similar pattern but requires the business number (Paybill number or Till number) as the account identifier. Paybill transfers may also require an account number field, since many Paybills use account numbers to route payments internally.
Bank account transfers
Standard bank transfers in Kenya work with Kenyan bank codes and KES currency. Paystack routes these through the standard interbank clearing system. Settlement times depend on the destination bank and can range from minutes to several hours.
Pesalink transfers
Pesalink is Kenya's real-time interbank transfer network. When available through Paystack, Pesalink transfers arrive in the recipient's bank account within seconds rather than the hours that standard interbank transfers can take. Pesalink has per-transaction limits set by the network, so it is best suited for transfers below those thresholds. For implementation details, see Pesalink transfers through Paystack.
Kenya is where Paystack's transfer capabilities get the most interesting because of the variety of destinations. Most other markets only support bank accounts and (in Ghana's case) mobile money. For M-Pesa-specific implementation patterns, see transfers to M-Pesa wallets, Paybills, and Tills.
Ghanaian Mobile Money Transfers
In Ghana, Paystack supports transfers to both bank accounts (using the ghipss recipient type) and mobile money wallets (using the mobile_money recipient type). Mobile money is the more common payout method for most Ghanaian use cases.
To create a mobile money recipient in Ghana, you specify the mobile money provider and the phone number. Paystack supports the major providers: MTN Mobile Money, Vodafone Cash, and AirtelTigo Money.
The process follows the same pattern as bank transfers: create the recipient, get a recipient_code, then initiate a transfer to that code. The amount is in pesewas (1 GHS = 100 pesewas).
Mobile money transfers in Ghana typically settle faster than bank transfers. The recipient gets a notification from their mobile money provider when the funds arrive. Failed transfers happen when the phone number is not registered for mobile money, the account has been deactivated, or the provider's systems are down.
One thing to watch for with Ghanaian mobile money: phone number portability. A user might have ported their number from one provider to another, but if you created the recipient with the old provider, the transfer will fail. Always confirm the current provider with the user when creating recipients.
Checking Your Balance Before Transfers
Every transfer draws from your available Paystack balance. If you try to send more than you have, the transfer fails. For single transfers, this is an inconvenience. For bulk transfers, insufficient balance can fail the entire batch.
Check your balance before initiating transfers:
const checkBalance = async () => {
const response = await fetch('https://api.paystack.co/balance', {
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
},
});
const data = await response.json();
// data.data is an array of balances, one per currency
// Each balance has: currency, balance (total)
return data.data;
};
const balances = await checkBalance();
// [{ currency: 'NGN', balance: 5000000 }, { currency: 'KES', balance: 250000 }]
The balance field is in the smallest currency unit (kobo for NGN). So 5000000 is NGN 50,000.
For automated payout systems, build the balance check into your payout pipeline. Before you assemble a batch of transfers, fetch the balance, sum up the total payout amount, and compare. If the balance is insufficient, either hold the batch until more funds arrive or split it into smaller batches that fit within the available balance.
If you run payouts on a schedule (weekly vendor payouts, monthly salary), consider checking your balance a few hours before the scheduled run. This gives you time to fund the balance manually or adjust the payout queue if needed. Surprises at payout time make for stressful weekends.
Building an Automated Payout System
A real payout system is more than a loop calling the transfer API. You need queuing, status tracking, failure handling, approval workflows, and reconciliation. Here is the architecture that works in production.
Step 1: Build a payout queue
Every payout request goes into a database table first. The table tracks the recipient, amount, status, reference, Paystack transfer code, and timestamps. Nothing goes to Paystack until the payout is approved and the system is ready to execute.
Step 2: Approval and review
For any payout system handling meaningful amounts, add an approval step. This can be a simple admin dashboard where someone reviews pending payouts before execution, or a maker-checker flow where one person creates the payout and another approves it. This is your safety net when OTP is disabled.
Step 3: Batch execution
When approved payouts are ready to run, your system should: check the available balance, assemble payouts into batches of up to 100, send each batch via the bulk transfer endpoint, record the transfer codes and initial statuses, and wait for webhooks.
Step 4: Webhook-driven status updates
Your webhook handler receives transfer.success and transfer.failed events. Match each event to your payout record using the reference, update the status, and trigger any downstream actions (notify the recipient, update their dashboard balance, mark the payout as complete).
Step 5: Reconciliation
At the end of each payout cycle, compare your database records against Paystack's transfer list API. Look for any payouts stuck in pending that never received a webhook (webhook delivery can fail), payouts marked success in your system but failed on Paystack's end (stale data), or transfers on Paystack that do not exist in your database (should not happen, but investigate if it does).
This is a simplified overview. For a full implementation guide with code examples, database schema, and edge case handling, see building an automated payout system on Paystack.
Preventing Duplicate Payouts with Idempotency
Duplicate payouts are the most expensive bug in any transfer system. If your code sends the same payout twice, you have paid someone double, and getting the money back depends on the recipient's goodwill. Prevention is everything.
Paystack uses the reference field for transfer idempotency. If you send two transfer requests with the same reference, Paystack treats the second as a duplicate and returns the existing transfer instead of creating a new one. This is your primary defense.
Designing idempotent references
Your reference should be deterministic: given the same payout inputs, you always generate the same reference. Good patterns:
// Pattern 1: Entity-based
const reference = `payout_vendor_${vendorId}_${payoutPeriod}`;
// Example: payout_vendor_42_2026-07
// Same vendor + same period = same reference = no duplicate
// Pattern 2: Payout record ID
const reference = `payout_${payoutRecordId}`;
// Example: payout_8847
// Your database record ID guarantees uniqueness
// Pattern 3: Hash-based for complex inputs
const crypto = require('crypto');
const hash = crypto.createHash('sha256')
.update(`${vendorId}_${amount}_${period}`)
.digest('hex')
.slice(0, 16);
const reference = `pay_${hash}`;
Pattern 2 is the simplest and most reliable. Create the payout record in your database first, use its ID as the reference, and then call Paystack. If anything goes wrong and you retry, the same record ID produces the same reference, and Paystack deduplicates it.
Beyond the reference: application-level safeguards
The reference handles Paystack-side deduplication, but you also need application-level checks:
- Before initiating a transfer, check your database to see if this payout has already been sent.
- Use database constraints (unique index on the reference column) to prevent your own code from creating duplicate payout records.
- If you process payouts from a queue, make sure the queue consumer is idempotent. Processing the same message twice should not create a second transfer.
For a deep dive into reference design, race condition prevention, and testing your idempotency guarantees, see preventing duplicate payouts: idempotency for transfers.
What This Cluster Covers
This guide is the hub for everything about sending money out through Paystack. Each topic has a dedicated deep-dive article with full code examples, edge cases, and production patterns. Here is how the articles in this cluster are organized:
Core transfer concepts
- How Paystack transfers work end to end: The full lifecycle from balance to recipient's account
- Creating transfer recipients: Bank accounts, mobile money, and validation
- Single transfers: One recipient, one transfer, full code walkthrough
- Bulk transfers: Batching, limits, and tracking 100 transfers at a time
Security and control
- Transfer OTP: enabling, disabling, and automating: Managing the OTP requirement for automated systems
- Preventing duplicate payouts: Reference design and idempotency patterns
- Managing failures and reversals: What to do when transfers go wrong
Country-specific transfers
- M-Pesa wallets, Paybills, and Tills: Kenya's mobile money payout options
- Pesalink transfers: Real-time interbank transfers in Kenya
Building payout systems
- Building an automated payout system: Architecture, queuing, and reconciliation
For the full picture of how transfers fit into the broader Paystack platform alongside payments, webhooks, and verification, see the Paystack engineering guide.
Key Takeaways
- ✓Paystack Transfers move money from your Paystack balance to external bank accounts and mobile money wallets. You need a positive balance before any transfer can go out.
- ✓Every transfer goes to a transfer recipient. You create the recipient once with their account details, then reference the recipient code for all future transfers. Never pass raw account numbers in transfer requests.
- ✓Transfers require OTP confirmation by default. For automated payout systems, you can disable OTP through the Paystack dashboard or API after meeting their verification requirements.
- ✓Bulk transfers let you send money to up to 100 recipients in a single API call. Each transfer in the batch succeeds or fails independently.
- ✓In Kenya, Paystack supports transfers to bank accounts, M-Pesa wallets, Paybills, Tills, and Pesalink. Each destination type uses a different recipient type or account format.
- ✓Transfer statuses move through pending, otp, success, failed, and reversed. Listen for the transfer.success and transfer.failed webhook events to update your records.
- ✓Idempotency is critical for payouts. Use a unique reference for every transfer to prevent duplicate payments when retries happen.
Frequently Asked Questions
- How long do Paystack transfers take to arrive?
- Transfer speed depends on the destination. Nigerian bank transfers typically settle within minutes to a few hours, though some banks take longer during off-peak hours or maintenance windows. M-Pesa transfers in Kenya are usually near-instant. Pesalink transfers arrive in seconds. Ghanaian mobile money transfers are also fast, usually within minutes. Bank transfers in other markets vary. Paystack does not guarantee specific settlement times because the receiving bank or mobile money provider controls the final leg.
- Can I send transfers to recipients in a different country than my Paystack business?
- Paystack transfers are generally limited to the country where your Paystack business is registered. If your business is registered in Nigeria, you can send transfers to Nigerian bank accounts. For cross-border payouts, you would need Paystack business accounts in each country you want to pay out to, or consider alternative cross-border payment providers.
- What happens if a transfer fails after showing as successful?
- This is a reversal. Paystack sends a transfer.reversed webhook event, and the funds are returned to your Paystack balance. Reversals are rare but can happen due to issues on the receiving bank's end. Your system should handle the transfer.reversed event by updating the payout status in your database and potentially queuing the transfer for retry or notifying the recipient.
- Is there a minimum or maximum transfer amount on Paystack?
- Paystack enforces minimum and maximum transfer amounts that vary by currency and destination type. The exact limits can change and may depend on your account tier and verification level. Check the Paystack dashboard or contact their support for the current limits that apply to your account. Your payout system should validate amounts against these limits before sending transfers to the API.
- Can I schedule transfers to go out at a specific time?
- The Paystack API does not have a built-in scheduling feature for transfers. All transfers are initiated immediately when you call the API. To schedule payouts, build scheduling into your application: store the desired payout time in your database, run a cron job or scheduled task that checks for due payouts, and initiate the transfers at the right time. This gives you more control over timing, batching, and approval workflows than a built-in scheduler would.
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