How Paystack Transfers Work End to End
A Paystack transfer moves money from your available Paystack balance to a bank account or mobile money wallet. The process involves creating a transfer recipient, initiating a transfer to that recipient, confirming via OTP if required, and then waiting for Paystack to settle through the banking network. You track outcomes using webhooks for transfer.success, transfer.failed, and transfer.reversed events.
What a Paystack Transfer Actually Is
A Paystack transfer is the opposite of accepting a payment. When you accept payments, money flows from a customer into your Paystack balance. A transfer sends money out of that balance to someone else's bank account or mobile money wallet.
This is how you pay vendors, refund customers, disburse salaries, settle marketplace sellers, or run any operation where money needs to leave your platform and reach a real person's account.
Transfers happen through Paystack's connection to the banking infrastructure in each market. In Nigeria, Paystack routes through NIBSS (the Nigeria Inter-Bank Settlement System). In Kenya, transfers go through the local clearing house, Pesalink, or Safaricom for M-Pesa. In Ghana, through the Ghana Interbank Payment and Settlement System (GhIPSS) or mobile money providers. The destination determines the route, the speed, and the possible failure modes.
The money comes from your available balance, not your total balance. If customers paid you NGN 1,000,000 today but NGN 700,000 is still pending settlement from Paystack to your balance, you can only transfer from the remaining NGN 300,000. This distinction trips people up. Your dashboard might show a large total, but the transferable amount is often smaller.
For the complete engineering guide covering all Paystack transfer topics, see the Paystack transfers and payouts complete guide.
The Four-Stage Transfer Lifecycle
Every Paystack transfer follows four stages. Understanding each one is critical for building reliable payout systems.
Stage 1: Create a transfer recipient
Before you can send money to anyone, you register them as a transfer recipient. This stores their bank account or mobile money details and gives you a reusable recipient_code. You only do this once per destination account.
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',
name: 'Amina Okafor',
account_number: '0123456789',
bank_code: '058',
currency: 'NGN',
}),
});
const data = await response.json();
// Store data.data.recipient_code in your database
// e.g., 'RCP_abc123def456'
Paystack validates the account at creation time by checking with the bank. If the account number and bank code do not match, the request fails. This is your first defense against sending money to the wrong place.
Stage 2: Initiate the transfer
With a recipient code in hand, you call the transfer endpoint. You specify the amount, the recipient, and a unique reference.
const transferResponse = 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: 500000, // 5,000 NGN in kobo
recipient: 'RCP_abc123def456',
reason: 'Vendor payout - July week 3',
reference: 'payout_vendor42_2026w29',
}),
});
const transferData = await transferResponse.json();
// transferData.data.transfer_code => 'TRF_xxxxxxxx'
// transferData.data.status => 'otp' or 'pending'
At this point, the money is deducted from your available balance. If the transfer eventually fails, it comes back. But right now, it is gone from your perspective.
Stage 3: OTP confirmation (if enabled)
If OTP is enabled on your Paystack account (the default), the transfer status comes back as otp. Paystack sends a one-time code to the phone or email registered on your business account. You must submit it to finalize.
const otpResponse = await fetch('https://api.paystack.co/transfer/finalize_transfer', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
transfer_code: 'TRF_xxxxxxxx',
otp: '123456',
}),
});
If OTP is disabled, this stage is skipped entirely. The transfer goes straight to pending.
Stage 4: Banking network settlement
Paystack routes the transfer through the appropriate banking channel. The money moves from Paystack's settlement account to the recipient's bank or mobile money provider. This is the part you do not control. It depends on the destination bank's processing speed, the time of day, and whether there are any network issues.
Transfer Status Transitions
A transfer moves through a defined set of statuses. Each status tells you exactly where the money is in the lifecycle.
- otp: The transfer is waiting for OTP confirmation. Money has been reserved from your balance. Only appears when OTP is enabled on your account.
- pending: The transfer has been confirmed (or OTP was not required) and Paystack is processing it through the banking network. Money has left your balance but has not reached the recipient.
- success: The money has been delivered to the recipient's account. This is the final happy state.
- failed: The transfer could not be completed. The amount is returned to your Paystack balance. Common reasons include invalid account details, bank downtime, or recipient account restrictions.
- reversed: The transfer was initially marked as successful but was later pulled back. This is rare and usually happens because of issues on the receiving bank's side. The money is returned to your balance.
The normal flow is: otp (if enabled) -> pending -> success. The failure path is: otp (if enabled) -> pending -> failed. Reversals are: success -> reversed.
Each status change fires a corresponding webhook event. Your system should not poll the transfer status endpoint in a loop. Instead, set up webhook handlers and let Paystack push the updates to you.
// Webhook handler for transfer events
app.post('/webhooks/paystack', (req, res) => {
// Always return 200 immediately
res.sendStatus(200);
const event = req.body;
if (event.event === 'transfer.success') {
const reference = event.data.reference;
// Mark payout as completed in your database
// Notify the recipient
}
if (event.event === 'transfer.failed') {
const reference = event.data.reference;
const reason = event.data.reason;
// Mark payout as failed
// Queue for retry if the reason is transient
}
if (event.event === 'transfer.reversed') {
const reference = event.data.reference;
// Mark payout as reversed
// Alert your operations team
}
});
The reversal case deserves special attention. If you already told a vendor their payout succeeded and it later reverses, you need a process for communicating the reversal and re-initiating the transfer. For handling strategies, see managing transfer failures and reversals.
Timing and Settlement Speed by Destination
How fast a transfer arrives depends entirely on the destination. Paystack processes the transfer on their end quickly, but the last mile through the banking network varies.
Nigerian bank accounts (NUBAN)
Most transfers settle within minutes during business hours. Some banks take longer, especially during peak periods or maintenance windows. Overnight transfers may process the next business day depending on the receiving bank's batch processing schedule.
M-Pesa wallets (Kenya)
Near-instant. Once Paystack submits to Safaricom, the recipient gets the M-Pesa notification within seconds. This is one of the fastest payout options available through Paystack.
Kenyan bank accounts
Standard interbank transfers in Kenya can take anywhere from minutes to several hours, depending on the destination bank and the clearing cycle. Business hours and weekday vs weekend timing affect speed.
Pesalink (Kenya)
Real-time. Pesalink was built specifically for instant interbank transfers. The recipient sees the money in their bank account within seconds. Pesalink has per-transaction limits, so it is best for transfers below those thresholds.
Ghanaian bank accounts (GhIPSS)
Settlement times vary by bank. Same-bank transfers are usually fast. Cross-bank transfers go through GhIPSS and can take minutes to hours.
Ghanaian mobile money
Mobile money transfers in Ghana (MTN MoMo, Vodafone Cash, AirtelTigo) are generally fast, settling within minutes. The recipient receives a notification from their provider.
Build your payout system with the assumption that any transfer might take up to 24 hours, even if most settle in minutes. Set user expectations accordingly. If you promise instant payouts and a transfer takes four hours because the destination bank is running slow, your support queue fills up. Under-promise and over-deliver on settlement speed.
Balance Deduction and Refund Mechanics
Understanding when money leaves and returns to your balance prevents confusion and accounting errors.
When money leaves your balance
Money is deducted from your available balance at the moment you initiate the transfer, not when the recipient receives it. If you initiate a transfer for NGN 50,000, your available balance drops by NGN 50,000 immediately, even though the recipient might not see the money for hours.
This means that if you check your balance right after initiating a transfer, it will already reflect the deduction. If you are running batch payouts, each transfer reduces your available balance in sequence. Your second transfer only has access to whatever remains after the first.
When money comes back
If a transfer fails, Paystack returns the amount to your balance. The refund is automatic. You do not need to do anything to trigger it. The transfer.failed webhook tells you the transfer did not go through, and by the time you receive it, the money is back in your balance.
Reversals work similarly. If a transfer that was marked as successful gets reversed (the destination bank returns it), the money comes back to your balance and you receive a transfer.reversed webhook.
The gap between deduction and confirmation
There is always a window between when the money leaves your balance (initiation) and when you get confirmation of the outcome (success or failure webhook). During this window, the money is in transit. Your balance shows the deduction, but you do not know the final outcome yet.
For accounting purposes, treat initiated transfers as "in flight" until you receive a definitive webhook. Do not mark a payout as complete just because the API call returned 200. The API acceptance only means Paystack received your request and reserved the funds. Final confirmation comes through the webhook.
// Good: Track the in-flight state
async function initiatePayout(payoutId) {
const payout = await db.payouts.findById(payoutId);
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',
amount: payout.amountInSmallestUnit,
recipient: payout.recipientCode,
reference: 'payout_' + payout.id,
}),
});
const data = await response.json();
// Mark as in-flight, not complete
await db.payouts.update(payoutId, {
status: 'processing',
transferCode: data.data.transfer_code,
initiatedAt: new Date(),
});
}
Webhook Events You Must Handle
Paystack sends three webhook events for transfers. Handling all three is not optional if you want a reliable payout system.
transfer.success
The money reached the recipient. Update your payout record to completed, notify the recipient if needed, and log the event for reconciliation. This is the event you will see most often.
transfer.failed
The transfer could not be completed. The payload includes a reason field explaining why: invalid account, insufficient balance, bank rejection, or network timeout. Update your payout record, decide whether to retry (transient errors like bank downtime) or escalate (permanent errors like invalid accounts), and alert the appropriate person.
transfer.reversed
A previously successful transfer was reversed. This is uncommon but critical to handle. The money is back in your balance, but you may have already told the recipient they were paid. Your handler needs to update the payout record, alert your operations team, and potentially trigger communication to the recipient.
All three events share the same payload structure. The data.reference field matches the reference you provided when initiating the transfer. Use this to look up the corresponding payout record in your database.
// Match webhook to your payout record
async function handleTransferWebhook(event) {
const reference = event.data.reference;
const payout = await db.payouts.findByReference(reference);
if (!payout) {
console.error('No payout found for reference: ' + reference);
return;
}
switch (event.event) {
case 'transfer.success':
await db.payouts.update(payout.id, {
status: 'completed',
completedAt: new Date(event.data.updated_at),
});
break;
case 'transfer.failed':
await db.payouts.update(payout.id, {
status: 'failed',
failureReason: event.data.reason,
failedAt: new Date(event.data.updated_at),
});
break;
case 'transfer.reversed':
await db.payouts.update(payout.id, {
status: 'reversed',
reversedAt: new Date(event.data.updated_at),
});
// Alert operations team
break;
}
}
One critical point: always verify the webhook signature before processing. Paystack signs every webhook with your secret key. If you skip verification, anyone can POST fake events to your endpoint and trick your system into marking payouts as complete when they are not.
Common Failure Scenarios and What Causes Them
Transfers fail for a limited set of predictable reasons. Knowing them lets you build specific handling for each.
Insufficient balance
You tried to send more than your available balance allows. This fails immediately at the API level before the transfer even enters the banking network. Prevention: always check your balance before initiating transfers, especially before bulk payouts.
Invalid or closed account
The recipient's bank account no longer exists, has been closed, or the account number was wrong from the start. Even though Paystack validates accounts at recipient creation time, accounts can close between when you created the recipient and when you initiate the transfer. For long-lived recipients (vendors who have been on your platform for months), periodic revalidation is worth considering.
Bank downtime or network issues
The destination bank's systems are unavailable. This happens more often than you might expect, especially during maintenance windows (often late at night or early morning). Transfers that fail for this reason can usually be retried successfully after a delay.
Recipient account restrictions
The recipient's account has restrictions that prevent it from receiving transfers. This could be a frozen account, a dormant account, or an account that has hit its daily inflow limit. You cannot fix this from your side. The recipient needs to resolve it with their bank.
Daily or transaction limits exceeded
Some bank accounts have daily inflow limits, especially lower-tier accounts in markets where tiered KYC applies. A transfer that would push the recipient over their daily limit will fail even if the account is otherwise valid.
For each failure, check the reason field in the transfer.failed webhook. Build your retry logic around the reason: retry for transient errors (bank downtime, timeouts), do not retry for permanent errors (invalid account, restrictions). For detailed retry patterns, see managing transfer failures and reversals.
Transfer vs Split Settlement: When to Use Which
Paystack has two ways to send money to third parties: transfers and split payments. They solve different problems, and picking the wrong one creates unnecessary work.
Use transfers when:
- You are sending money that is already in your balance (payroll, vendor payouts after you have collected payments).
- The payout amount is calculated after the fact, not at the point of customer payment.
- You need to control exactly when money goes out (scheduled payouts, approval workflows).
- You are paying people who have nothing to do with a specific customer transaction (employees, contractors).
Use split payments when:
- The payout is directly tied to a customer payment and can be calculated at the point of transaction.
- You want the money to go directly from the customer to the third party without passing through your balance.
- You are building a marketplace where vendors should be settled automatically as customers pay.
Many platforms use both. A food delivery app might use splits to route the restaurant's share at checkout, but use transfers to pay riders at the end of the day based on completed deliveries. The two features are complementary, not competing.
The key difference is timing and control. Splits happen at payment time and are automatic. Transfers happen when you initiate them and give you full control over the when and how much. For marketplace split patterns, see the split payments and marketplaces complete guide.
Debugging a Transfer That Seems Stuck
Sometimes a transfer sits in pending for longer than expected, and you are not sure if it is still processing or if something went wrong. Here is how to investigate.
Step 1: Fetch the transfer status
const checkTransfer = async (transferIdOrCode) => {
const response = await fetch(
'https://api.paystack.co/transfer/' + transferIdOrCode,
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
);
const data = await response.json();
return data.data;
};
const transfer = await checkTransfer('TRF_abc123');
console.log('Status:', transfer.status);
console.log('Updated at:', transfer.updated_at);
Step 2: Check if you missed a webhook
Webhooks can fail to deliver for many reasons: your server was down, the request timed out, your firewall blocked it. If a transfer shows as success or failed on Paystack but your database still says pending, you missed the webhook. Update your record based on the API response and investigate why the webhook did not arrive.
Step 3: Check the Paystack dashboard
Log into your Paystack dashboard and look at the transfer details. The dashboard shows the full history including any bank responses. This often gives more detail than the API response about why a transfer is taking long or what went wrong.
Step 4: Check for known bank issues
Bank processing delays are common in several African markets. Before escalating to Paystack support, check if there are known issues with the destination bank. Paystack's status page or social media channels sometimes report banking partner outages.
If a transfer has been pending for more than 24 hours, contact Paystack support with the transfer code and reference. They can investigate with the banking partner on the receiving end.
Key Takeaways
- ✓Paystack transfers move money from your available balance to external bank accounts or mobile money wallets. The available balance excludes funds still pending settlement.
- ✓The lifecycle has four stages: create recipient, initiate transfer, confirm OTP (if enabled), and wait for banking network settlement.
- ✓Transfer statuses progress through otp, pending, success, failed, or reversed. Each transition fires a webhook event you should handle.
- ✓Money leaves your balance at initiation, not at success. If a transfer fails, the amount is returned to your balance automatically.
- ✓Always use webhook events as the source of truth for transfer outcomes rather than polling the API.
- ✓Transfer timing depends on the destination. Nigerian bank transfers can take minutes to hours. M-Pesa is near-instant. Pesalink settles in seconds.
- ✓Failed transfers are refunded automatically. Reversed transfers (initially successful but later pulled back) are rarer and require different handling.
Frequently Asked Questions
- Does Paystack charge a fee for transfers?
- Paystack charges transfer fees that vary by market, destination type, and amount. The fee structure can change, so check the Paystack pricing page or your dashboard for current rates. Transfer fees are deducted from your balance on top of the transfer amount, so factor them into your payout calculations.
- Can I cancel a transfer after initiating it?
- Once a transfer moves past the OTP stage into pending, you cannot cancel it through the API. The money is already in the banking network. If OTP is enabled and the transfer is still in the otp state, you can effectively cancel by not submitting the OTP. The transfer will eventually expire.
- What happens if I initiate a transfer to the wrong person?
- If the transfer is still awaiting OTP, do not submit the OTP and it will expire. If the transfer has already been sent, there is no automated way to recall it. You would need to contact the recipient directly or work with Paystack support and the receiving bank to attempt recovery. This is why validating recipient details (showing the resolved account name to your user before confirming) is so important.
- How do I know if my Paystack balance is sufficient for a transfer?
- Call GET /balance to check your available balance in each currency. The response returns an array of balances. Compare the available amount in the relevant currency against the transfer amount plus any transfer fees. If the balance is insufficient, the transfer will fail at the API level.
- Can I send transfers on weekends and public holidays?
- You can initiate transfers at any time through the API. Whether the transfer settles on a weekend or holiday depends on the destination. M-Pesa operates 24/7. Bank transfers may be queued until the next business day depending on the receiving bank and the interbank settlement system schedule in that market.
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