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.
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:
recipient_code you will use for all future transfers to that person.transfer_code.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.
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:
nuban for Nigerian bank accountsghipss for Ghanaian bank accountsmobile_money for mobile money wallets (Ghana)basa for South African bank accountsauthorization for 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.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
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:
source is always "balance". There is no other valid value.amount is 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.reason is 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.reference is 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.
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:
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.
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:
POST /transfer/disable_otp to request OTP disabling.POST /transfer/disable_otp_finalize with that OTP.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.
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:
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.
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.
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.
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.
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.
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:
For a deep dive into reference design, race condition prevention, and testing your idempotency guarantees, see preventing duplicate payouts: idempotency for transfers.
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
Security and control
Country-specific transfers
Building payout systems
For the full picture of how transfers fit into the broader Paystack platform alongside payments, webhooks, and verification, see the Paystack engineering guide.
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