McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Paystack Refund Failed or Stuck
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Paystack Refund Failed or Stuck

A Paystack refund fails when the original transaction is too old (beyond the refund window), the partial refund amount exceeds the remaining refundable balance, or the payment channel does not support automated refunds. A refund stuck at "pending" means the bank or payment provider has not processed it yet, which can take 5 to 10 business days for card refunds. Check the refund status via the API, wait for the refund.failed or refund.processed webhook, and use a manual transfer to the customer bank account as a last resort.

In this article

  1. 1. How Paystack Refunds Work
  2. 2. Every Reason a Refund Fails
  3. 3. When a Refund Is Stuck at "Pending"
  4. 4. Handling Refund Webhooks
  5. 5. Manual Refund via Transfer (The Fallback)
  6. 6. Tracking Partial Refunds
  7. 7. What to Tell the Customer
  8. 8. Verification: Confirm Your Refund Flow Works

How Paystack Refunds Work

A Paystack refund reverses a previous successful transaction. The process is asynchronous:

  1. You call POST /refund with the transaction reference or ID
  2. Paystack validates the request (checks amount, channel, age, etc.)
  3. If valid, Paystack returns a 200 with the refund in "pending" status
  4. Paystack sends the refund instruction to the bank or payment provider
  5. The bank processes the refund (1 to 10 business days for cards)
  6. Paystack sends a refund.processed or refund.failed webhook

The API returning a 200 at step 3 does NOT mean the refund is complete. It means Paystack accepted the request. The refund can still fail at step 5 if the bank rejects it.

// Initiating a refund
const res = await fetch('https://api.paystack.co/refund', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    transaction: 'txn_abc123',  // Transaction reference or ID
    amount: 250000,              // Amount in kobo (optional, defaults to full refund)
  }),
});

const data = await res.json();
// data.data.status will be "pending"

Every Reason a Refund Fails

1. Transaction too old

Banks have a window within which refunds can be processed. For card transactions, this is typically 90 to 180 days. Attempting to refund a transaction beyond this window fails because the bank will not accept the reversal instruction.

// Error response for an old transaction
{
  "status": false,
  "message": "Transaction is not eligible for refund"
}

2. Partial refund exceeds remaining balance

If you already issued partial refunds on a transaction, the total of all refunds cannot exceed the original transaction amount. For example, on a 10,000 kobo transaction where you already refunded 7,000 kobo, attempting to refund 5,000 kobo fails because only 3,000 kobo is left.

// Error when partial refund exceeds remaining
{
  "status": false,
  "message": "Refund amount is more than the remaining amount on this transaction"
}

3. Transaction was not successful

You can only refund successful transactions. Attempting to refund a failed, abandoned, or pending transaction returns an error.

4. Channel does not support automated refunds

Not all payment channels support automated refunds through the API. Mobile money refunds and bank transfer refunds may not be available depending on the provider. Card refunds are the most reliable.

5. Duplicate refund

Attempting to fully refund a transaction that has already been fully refunded returns an error. Paystack blocks the second refund to prevent double-refunding.

6. Insufficient Paystack balance

Refunds are funded from your Paystack balance. If your balance is too low to cover the refund amount, the refund fails.

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

When a Refund Is Stuck at "Pending"

A "pending" refund is not necessarily stuck. Card refunds can legitimately take 5 to 10 business days. Here is how to tell the difference between normal processing and an actual problem:

Normal pending (1 to 10 business days): The refund was accepted by Paystack and sent to the bank. The bank is processing it. This is the standard flow. Do not panic.

Abnormally pending (more than 15 business days): If a refund has been pending for more than 15 business days, something may be wrong. Contact Paystack support with the refund reference.

Check the current status of a refund through the API:

// Check refund status
async function checkRefundStatus(refundId: string) {
  const res = await fetch(
    `https://api.paystack.co/refund/${refundId}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
      },
    }
  );

  const data = await res.json();
  const refund = data.data;

  console.log('Refund status:', refund.status);
  console.log('Created at:', refund.createdAt);
  console.log('Amount:', refund.amount / 100, 'NGN');

  // Calculate days since refund was created
  const daysSinceCreated = Math.floor(
    (Date.now() - new Date(refund.createdAt).getTime()) / (1000 * 60 * 60 * 24)
  );

  if (refund.status === 'pending' && daysSinceCreated > 15) {
    console.warn(
      'Refund ' + refundId + ' has been pending for ' + daysSinceCreated + ' days. ' +
      'Contact Paystack support.'
    );
  }

  return refund;
}

Set up a scheduled job that checks all pending refunds older than 10 business days and alerts your team if any are found. Most will resolve on their own, but the alert catches the ones that need manual follow-up.

Handling Refund Webhooks

Paystack sends two refund-related webhook events:

  • refund.processed: The refund was completed. Money is on its way to the customer.
  • refund.failed: The refund could not be processed. You need to take action.
// Webhook handler for refund events
app.post('/api/webhooks/paystack', async (req, res) => {
  // Return 200 immediately
  res.status(200).json({ received: true });

  const event = req.body;

  switch (event.event) {
    case 'refund.processed': {
      const { transaction, amount } = event.data;
      const reference = transaction.reference;

      // Update refund record
      await db.query(
        `UPDATE refunds SET status = 'processed', processed_at = NOW()
         WHERE transaction_reference = $1`,
        [reference]
      );

      // Notify the customer
      const order = await getOrderByReference(reference);
      await sendRefundConfirmationEmail({
        email: order.customerEmail,
        amount: amount / 100,
        reference,
      });

      console.log(`Refund processed for ${reference}: ${amount / 100} NGN`);
      break;
    }

    case 'refund.failed': {
      const { transaction, amount } = event.data;
      const reference = transaction.reference;

      // Update refund record
      await db.query(
        `UPDATE refunds SET status = 'failed', failed_at = NOW()
         WHERE transaction_reference = $1`,
        [reference]
      );

      // Alert the team to investigate
      await sendSlackAlert({
        channel: '#payments',
        message: 'Refund failed for transaction ' + reference + '. ' +
          'Amount: ' + (amount / 100) + ' NGN. Manual intervention needed.',
      });

      console.error(`Refund failed for ${reference}`);
      break;
    }
  }
});

Do not notify the customer about a failed refund automatically. Your team should investigate first and determine the best recovery approach before communicating with the customer.

Manual Refund via Transfer (The Fallback)

When the automated refund fails or is not available for the payment channel, the fallback is to send money to the customer via a Paystack transfer. This bypasses the refund system entirely and sends the money directly from your Paystack balance to the customer's bank account.

// Manual refund via transfer
async function manualRefund(
  customerName: string,
  accountNumber: string,
  bankCode: string,
  amount: number,
  originalReference: string,
  reason: string
) {
  // Step 1: Validate the customer's bank account
  const validation = await fetch(
    `https://api.paystack.co/bank/resolve?account_number=${accountNumber}&bank_code=${bankCode}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
      },
    }
  );

  const validationData = await validation.json();
  if (!validationData.status) {
    throw new Error('Could not validate customer bank account: ' + validationData.message);
  }

  // Step 2: Create transfer recipient
  const recipientRes = 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: validationData.data.account_name,
      account_number: accountNumber,
      bank_code: bankCode,
      currency: 'NGN',
    }),
  });

  const recipientData = await recipientRes.json();
  if (!recipientData.status) {
    throw new Error('Could not create recipient: ' + recipientData.message);
  }

  // Step 3: Initiate transfer
  const transferRes = 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,
      recipient: recipientData.data.recipient_code,
      reason: `Refund for transaction ${originalReference}: ${reason}`,
      reference: `refund_manual_${originalReference}_${Date.now()}`,
    }),
  });

  const transferData = await transferRes.json();

  // Step 4: Record the manual refund
  await db.query(
    `INSERT INTO manual_refunds
     (original_reference, transfer_reference, recipient_account, amount, reason, status, created_at)
     VALUES ($1, $2, $3, $4, $5, 'pending', NOW())`,
    [originalReference, transferData.data.reference, accountNumber, amount, reason]
  );

  return transferData;
}

Important considerations for manual refunds:

  • You need the customer's bank account details (account number and bank). Collect this from the customer before initiating the transfer.
  • The transfer incurs a fee that you absorb. Factor this into your refund economics.
  • Track manual refunds separately from automated refunds in your database for accounting purposes.
  • Get internal approval before issuing manual refunds over a certain threshold to prevent fraud.

Tracking Partial Refunds

Partial refunds require careful tracking. You need to know how much has already been refunded so you do not exceed the original transaction amount.

// Database schema for refund tracking
// CREATE TABLE refunds (
//   id SERIAL PRIMARY KEY,
//   transaction_reference VARCHAR(100) NOT NULL,
//   refund_reference VARCHAR(100) UNIQUE,
//   amount INTEGER NOT NULL,  -- in kobo
//   type VARCHAR(20) NOT NULL DEFAULT 'automated',  -- 'automated' or 'manual'
//   status VARCHAR(20) NOT NULL DEFAULT 'pending',
//   reason TEXT,
//   created_at TIMESTAMPTZ DEFAULT NOW(),
//   processed_at TIMESTAMPTZ
// );

async function initiatePartialRefund(
  transactionReference: string,
  refundAmount: number,
  reason: string
) {
  // Calculate how much has already been refunded
  const refunded = await db.query(
    `SELECT COALESCE(SUM(amount), 0) as total_refunded
     FROM refunds
     WHERE transaction_reference = $1
       AND status IN ('pending', 'processed')`,
    [transactionReference]
  );

  const totalRefunded = parseInt(refunded.rows[0].total_refunded);

  // Get original transaction amount
  const txRes = await fetch(
    `https://api.paystack.co/transaction/verify/${transactionReference}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
      },
    }
  );

  const txData = await txRes.json();
  const originalAmount = txData.data.amount;
  const remainingRefundable = originalAmount - totalRefunded;

  if (refundAmount > remainingRefundable) {
    throw new Error(
      'Cannot refund ' + (refundAmount / 100) + ' NGN. ' +
      'Original amount: ' + (originalAmount / 100) + ' NGN. ' +
      'Already refunded: ' + (totalRefunded / 100) + ' NGN. ' +
      'Maximum refundable: ' + (remainingRefundable / 100) + ' NGN.'
    );
  }

  // Proceed with refund
  const res = await fetch('https://api.paystack.co/refund', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      transaction: transactionReference,
      amount: refundAmount,
    }),
  });

  const data = await res.json();

  if (data.status) {
    await db.query(
      `INSERT INTO refunds (transaction_reference, refund_reference, amount, reason, status)
       VALUES ($1, $2, $3, $4, 'pending')`,
      [transactionReference, data.data.id, refundAmount, reason]
    );
  }

  return data;
}

What to Tell the Customer

Refund timing frustrates customers more than almost anything else. Set expectations clearly:

When a refund is initiated:

  • "Your refund of KES [amount] has been initiated. Card refunds typically take 5 to 10 business days to appear on your statement."
  • "Reference: [reference]. Keep this in case you need to follow up with your bank."

When a refund fails and you are doing a manual transfer:

  • "We were unable to process your refund through the original payment method. We will send the refund directly to your bank account. Please provide your bank account number and bank name."

When a customer says the refund has not arrived:

  • First, check the refund status on your end. If it shows "processed", the money was sent to the bank.
  • "Your refund was processed on [date]. It can take your bank up to 10 business days to credit your account. If you do not see it by [date + 10 business days], please contact your bank with reference [reference]."

Never tell a customer a refund is "instant". Even when Paystack processes the refund immediately, the customer's bank controls when the money appears in their account.

Verification: Confirm Your Refund Flow Works

Test each refund scenario in test mode:

  1. Full refund. Create a test transaction, then refund the full amount. Confirm the refund is created with "pending" status. Check the webhook handler receives the refund.processed event.
  2. Partial refund. Create a test transaction for 10,000 kobo. Refund 3,000 kobo. Confirm the refund succeeds. Then refund another 5,000 kobo. Confirm it succeeds. Then try to refund 5,000 kobo (which would exceed the remaining 2,000). Confirm the error message is clear.
  3. Duplicate refund. Fully refund a transaction, then try to refund it again. Confirm Paystack rejects the second refund with an appropriate error.
  4. Manual refund fallback. Simulate a failed automated refund by skipping the refund API call. Use the manual transfer flow instead. Confirm the transfer is created and the manual refund is recorded in your database.
  5. Refund status checking. Create a refund and call the status check endpoint. Confirm you can retrieve the refund details including status and timestamps.

For your accounting team, generate a monthly refund report showing automated vs. manual refunds, success vs. failure rates, and average processing time. This data helps you negotiate with Paystack support when refunds take longer than expected.

Key Takeaways

  • ✓Paystack card refunds can take 5 to 10 business days to reach the customer account. A refund stuck at "pending" during this period is normal, not an error.
  • ✓Refunds fail when the original transaction is older than the refund window, which varies by channel and bank. Card transactions can typically be refunded within 90 to 180 days.
  • ✓Partial refund errors occur when the amount you are trying to refund exceeds the remaining refundable balance. If you already refunded part of the transaction, subtract that from the original amount to find the maximum you can still refund.
  • ✓Mobile money and bank transfer refunds are less reliable than card refunds through the automated refund API. For these channels, a manual transfer to the customer is often the better approach.
  • ✓When the automated refund fails, initiate a transfer to the customer bank account as a fallback. This gives the customer their money while bypassing the refund system entirely.
  • ✓Always listen for the refund.processed and refund.failed webhook events. Do not assume a refund succeeded just because the API returned a 200 status.
  • ✓Log every refund attempt with the original transaction reference, refund amount, and reason. This audit trail is essential for customer disputes and accounting reconciliation.

Frequently Asked Questions

How long does a Paystack refund take to reach the customer?
Card refunds typically take 5 to 10 business days after Paystack processes the refund. The delay is on the banking side, not Paystack. Mobile money refunds can be faster (1 to 3 days). Manual refund transfers to bank accounts arrive within 24 hours in most cases.
Can I refund a Paystack transaction that is more than 6 months old?
Most likely not. Banks have a refund window that typically ranges from 90 to 180 days. Transactions older than this cannot be refunded through the automated refund API. Your only option is a manual transfer to the customer bank account, which is essentially a new payment from your balance, not a reversal of the original transaction.
What happens if a Paystack refund fails after being accepted?
The money returns to your Paystack balance. You will receive a refund.failed webhook event. The customer does not receive anything. You need to either retry the refund (if the failure was transient) or use the manual transfer fallback to send the money to the customer bank account.
Can I refund a bank transfer payment on Paystack?
Automated refunds for bank transfer payments are limited. Paystack may not support automated reversal for all bank transfer types. The most reliable approach for refunding bank transfer payments is the manual transfer fallback: collect the customer bank details and send a transfer from your Paystack balance.
How do I handle partial refunds in my accounting system?
Track each partial refund as a separate record linked to the original transaction. Store the refund amount, date, reason, and status. Your accounting system should show the original transaction amount, total refunded, and net amount for each transaction. This is essential for reconciliation and tax reporting.

Learn Paystack Integration

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

Not ready? Create Free Account

Keep reading

Paystack Errors and Troubleshooting: The Complete Reference

Every Paystack error you will hit in production, what causes it, and how to fix it. Authentication failures, webhook signature problems, transaction errors, transfer issues, and frontend bugs. Code-first solutions for each one.

Read more

Paystack Dispute Deadline Missed: What Happens

When you miss a Paystack dispute deadline, the money is automatically returned to the cardholder. This decision is final and cannot be reversed. This guide covers what happens, why it happens, and how to build alerts so it never happens to you.

Read more

Paystack Transfer Failed: Reasons and Recovery

Paystack transfers fail for specific, diagnosable reasons. This guide covers every failure reason, the exact webhook payload for each, and a production-ready recovery strategy with code.

Read more

Paystack Rate Limit and 429 Handling

Paystack returns 429 Too Many Requests when you exceed their API rate limit. This guide covers the actual limits, how to implement exponential backoff, when to cache responses, and how to batch operations to avoid hitting the limit.

Read more

Idempotent Paystack Webhook Handlers: The Complete Pattern

A complete guide to building idempotent Paystack webhook handlers. Covers why Paystack sends duplicate events, using transaction references as idempotency keys, database-level deduplication with a processed_events table, and the full production-ready pattern in Node.js.

Read more

Seeding Test Data for a Payments Feature

Good test data is essential for testing payment features. This guide covers creating Paystack test mode resources (plans, subaccounts, split codes) and seeding your application database with matching payment records for realistic test scenarios.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.