Skip to main content
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:
  1. Creating transfer recipients
  2. Initiating transfers to recipients
  3. Finalizing transfers with OTP (when required)
  4. 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.
1

Import and initialize

import { Paystack } from '@efobi/paystack';

const paystack = new Paystack(process.env.PAYSTACK_SECRET_KEY!);
2

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);
3

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
number
required
Amount to transfer in kobo (minimum: 1000 kobo = ₦10)
recipient
string
required
Transfer recipient code (starts with RCP_)
reason
string
Description of the transfer
reference
string
required
Unique transfer reference
currency
string
default:"NGN"
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

perPage
number
default:"50"
Number of transfers per page
page
number
default:"1"
Page number to retrieve
status
string
Filter by status: pending, success, failed, reversed
from
string
Start date (ISO 8601 format)
to
string
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

  1. Validate recipient codes before initiating transfers
  2. Use unique references for each transfer
  3. Monitor your balance before bulk transfers
  4. Handle OTP gracefully in your user interface
  5. Store transfer records in your database
  6. Implement webhooks to track transfer status in real-time
  7. Test with small amounts before going live
  8. 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');
  });