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.
How Paystack Refunds Work
A Paystack refund reverses a previous successful transaction. The process is asynchronous:
- You call
POST /refundwith the transaction reference or ID - Paystack validates the request (checks amount, channel, age, etc.)
- If valid, Paystack returns a 200 with the refund in "pending" status
- Paystack sends the refund instruction to the bank or payment provider
- The bank processes the refund (1 to 10 business days for cards)
- Paystack sends a
refund.processedorrefund.failedwebhook
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.
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:
- 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.processedevent. - 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.
- Duplicate refund. Fully refund a transaction, then try to refund it again. Confirm Paystack rejects the second refund with an appropriate error.
- 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.
- 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.
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