The Transfer API allows you to send money from your Paystack balance to bank accounts. This guide covers how to initiate and manage transfers.
Overview
Transfers enable you to send funds to your customers, vendors, or employees. The process involves:
- Creating transfer recipients
- Initiating transfers to recipients
- Finalizing transfers with OTP (when required)
- Tracking transfer status
Transfers require sufficient balance in your Paystack account. Ensure your account is funded before initiating transfers.
Prerequisites
Before sending transfers, you need:
- A funded Paystack balance
- Transfer recipients created (see Transfer Recipients API)
- OTP enabled (for first-time transfers)
Initiate a Transfer
Send money to a transfer recipient.
Import and initialize
import { Paystack } from '@efobi/paystack';
const paystack = new Paystack(process.env.PAYSTACK_SECRET_KEY!);
Initiate the transfer
const { data, error } = await paystack.transfer.initiate({
source: 'balance',
amount: 50000, // Amount in kobo (₦500.00)
recipient: 'RCP_xxxxxxxxxx', // Recipient code
reason: 'Payment for services',
reference: `TRF-${Date.now()}`,
currency: 'NGN'
});
if (error) {
// Handle OTP requirement
if (error.data?.code === 'REQUIRES_OTP') {
console.log('OTP required. Check your email/phone.');
console.log('Next step:', error.data?.meta?.nextStep);
return;
}
console.error('Transfer failed:', error);
return;
}
console.log('Transfer initiated!');
console.log('Transfer code:', data.data.transfer_code);
console.log('Status:', data.data.status);
Handle OTP (if required)
// If OTP is required, finalize with the code
const { data: finalData, error: finalError } =
await paystack.transfer.finalize({
transfer_code: data.data.transfer_code,
otp: '123456' // OTP from email/SMS
});
if (finalData && finalData.data.status === 'success') {
console.log('Transfer completed successfully!');
}
Transfer Parameters
source
string
default:"balance"
required
Source of funds (currently only balance is supported)
Amount to transfer in kobo (minimum: 1000 kobo = ₦10)
Transfer recipient code (starts with RCP_)
Description of the transfer
Unique transfer reference
Currency code: NGN, USD, GHS, ZAR, KES, or XOF
Finalize Transfer with OTP
Complete a transfer that requires OTP verification.
const { data, error } = await paystack.transfer.finalize({
transfer_code: 'TRF_xxxxxxxxxx',
otp: '123456'
});
if (data && data.data.status === 'success') {
console.log('Transfer finalized successfully!');
console.log('Amount transferred:', data.data.amount / 100);
console.log('Recipient:', data.data.recipient);
} else if (error) {
console.error('OTP verification failed:', error.data?.message);
}
OTP is typically required for the first transfer or when transferring to new recipients. After successful verification, subsequent transfers may not require OTP.
Bulk Transfers
Initiate multiple transfers in a single request.
const { data, error } = await paystack.transfer.initiateBulk({
source: 'balance',
transfers: [
{
amount: 50000,
recipient: 'RCP_xxxxxxxxxx',
reference: `TRF-001-${Date.now()}`
},
{
amount: 75000,
recipient: 'RCP_yyyyyyyyyy',
reference: `TRF-002-${Date.now()}`
},
{
amount: 100000,
recipient: 'RCP_zzzzzzzzzz',
reference: `TRF-003-${Date.now()}`
}
]
});
if (error && error.data?.code === 'REQUIRES_OTP') {
console.log('Bulk transfer requires OTP');
// Finalize with OTP
} else if (data) {
console.log(`Initiated ${data.data.length} transfers`);
data.data.forEach(transfer => {
console.log(`${transfer.reference}: ${transfer.status}`);
});
}
Bulk transfers are processed asynchronously. Monitor transfer status using webhooks or by polling the transfer endpoints.
List Transfers
Retrieve a paginated list of transfers.
const { data, error } = await paystack.transfer.list({
perPage: 50,
page: 1,
status: 'success',
from: '2024-01-01',
to: '2024-12-31'
});
if (data) {
console.log(`Found ${data.meta.total} transfers`);
data.data.forEach(transfer => {
console.log(`${transfer.reference}:`, {
amount: transfer.amount / 100,
status: transfer.status,
recipient: transfer.recipient.name,
createdAt: transfer.created_at
});
});
}
List Parameters
Number of transfers per page
Filter by status: pending, success, failed, reversed
Start date (ISO 8601 format)
End date (ISO 8601 format)
Get Single Transfer
Fetch details of a specific transfer.
const { data, error } = await paystack.transfer.getTransferById(
'TRF_xxxxxxxxxx' // Transfer code or ID
);
if (data) {
console.log('Transfer details:', {
amount: data.data.amount / 100,
status: data.data.status,
reference: data.data.reference,
reason: data.data.reason,
recipient: {
name: data.data.recipient.name,
accountNumber: data.data.recipient.details.account_number,
bankName: data.data.recipient.details.bank_name
},
transferredAt: data.data.transferred_at
});
}
Verify Transfer
Confirm the status of a transfer by reference.
const reference = 'TRF-12345';
const { data, error } = await paystack.transfer.verify(reference);
if (data) {
const transfer = data.data;
if (transfer.status === 'success') {
console.log('Transfer completed successfully!');
await updatePaymentRecord(reference, 'completed');
} else if (transfer.status === 'failed') {
console.log('Transfer failed:', transfer.failures);
await updatePaymentRecord(reference, 'failed');
} else if (transfer.status === 'reversed') {
console.log('Transfer was reversed');
await updatePaymentRecord(reference, 'reversed');
} else {
console.log('Transfer is still pending');
}
}
Transfer Status
Transfers can have the following statuses:
- pending - Transfer is being processed
- success - Transfer completed successfully
- failed - Transfer failed (check
failures field for reason)
- reversed - Transfer was reversed (funds returned to your balance)
- processing - Transfer is in progress
Error Handling
Handle common transfer errors:
try {
const { data, error } = await paystack.transfer.initiate({
source: 'balance',
amount: 50000,
recipient: 'RCP_xxxxxxxxxx',
reference: `TRF-${Date.now()}`
});
if (error) {
// Handle specific error codes
if (error.data?.code === 'REQUIRES_OTP') {
return {
requiresOtp: true,
transferCode: error.data?.data?.transfer_code,
message: 'Please enter the OTP sent to your email'
};
}
if (error.data?.message?.includes('insufficient balance')) {
return {
success: false,
message: 'Insufficient balance in your Paystack account'
};
}
return { success: false, message: error.data?.message };
}
return { success: true, transfer: data.data };
} catch (err) {
console.error('Unexpected error:', err);
return { success: false, message: 'Transfer failed' };
}
Best Practices
- Validate recipient codes before initiating transfers
- Use unique references for each transfer
- Monitor your balance before bulk transfers
- Handle OTP gracefully in your user interface
- Store transfer records in your database
- Implement webhooks to track transfer status in real-time
- Test with small amounts before going live
- Set up notifications for failed transfers
Common Scenarios
Payout to Vendor
async function payVendor(
vendorId: string,
amount: number,
invoiceId: string
) {
// Get vendor's recipient code from database
const vendor = await db.vendors.findById(vendorId);
const { data, error } = await paystack.transfer.initiate({
source: 'balance',
amount: amount * 100, // Convert to kobo
recipient: vendor.recipientCode,
reason: `Payment for invoice ${invoiceId}`,
reference: `PAY-${invoiceId}-${Date.now()}`
});
if (error?.data?.code === 'REQUIRES_OTP') {
// Store for later finalization
await db.pendingTransfers.create({
transferCode: error.data.data.transfer_code,
vendorId,
amount,
invoiceId
});
return { requiresOtp: true, transferCode: error.data.data.transfer_code };
}
if (data && data.data.status === 'success') {
await db.invoices.update(invoiceId, { status: 'paid' });
await notifyVendor(vendorId, 'payment_sent', amount);
}
return { success: true, transfer: data.data };
}
Salary Payments
async function processSalaryPayments(month: string, year: number) {
const employees = await db.employees.findAll({ active: true });
const transfers = employees.map(emp => ({
amount: emp.salary * 100,
recipient: emp.recipientCode,
reference: `SAL-${emp.id}-${month}-${year}`
}));
const { data, error } = await paystack.transfer.initiateBulk({
source: 'balance',
transfers
});
if (error?.data?.code === 'REQUIRES_OTP') {
console.log('Bulk salary payment requires OTP');
await notifyAdmin('otp_required', { month, year });
return;
}
if (data) {
// Log all transfers
for (const transfer of data.data) {
await db.salaryPayments.create({
employeeId: transfer.reference.split('-')[1],
transferCode: transfer.transfer_code,
amount: transfer.amount / 100,
status: transfer.status,
month,
year
});
}
console.log(`Processed ${data.data.length} salary payments`);
}
}
Refund Processing
async function processRefund(
transactionId: string,
refundAmount: number,
reason: string
) {
// Get customer's recipient code
const transaction = await db.transactions.findById(transactionId);
const customer = await db.customers.findByEmail(transaction.email);
if (!customer.recipientCode) {
// Create recipient first
throw new Error('Customer recipient not set up');
}
const { data, error } = await paystack.transfer.initiate({
source: 'balance',
amount: refundAmount * 100,
recipient: customer.recipientCode,
reason: `Refund: ${reason}`,
reference: `REF-${transactionId}-${Date.now()}`
});
if (data && data.data.status === 'success') {
await db.refunds.create({
transactionId,
amount: refundAmount,
transferCode: data.data.transfer_code,
reason,
status: 'completed'
});
await notifyCustomer(customer.email, 'refund_processed', {
amount: refundAmount,
reason
});
}
return data?.data;
}
Webhook Integration
Listen for transfer events in real-time:
paystack.webhook
.on('transfer.success', (data) => {
console.log('Transfer successful:', data.reference);
// Update your records
updateTransferStatus(data.reference, 'success');
})
.on('transfer.failed', (data) => {
console.log('Transfer failed:', data.reference);
// Handle failure
updateTransferStatus(data.reference, 'failed');
notifyAdminOfFailure(data);
})
.on('transfer.reversed', (data) => {
console.log('Transfer reversed:', data.reference);
// Handle reversal
updateTransferStatus(data.reference, 'reversed');
});