If you miss the deadline to respond to a Paystack dispute (chargeback), Paystack automatically resolves the dispute in the cardholder favour. The transaction amount is deducted from your balance and returned to the cardholder. This is irreversible. You cannot appeal, reopen, or contest the dispute after the deadline. Prevention is the only solution: set up the dispute.create webhook, pipe it to Slack or email immediately, and build an internal escalation process with deadlines before the Paystack deadline.
Here is the timeline of a missed dispute:
dispute.create webhook event and a dashboard notification.The resolution is final. There is no "I missed it by one hour, can you reopen it?" option. The card network (Visa, Mastercard) has strict timelines, and Paystack has to comply.
// What the dispute.create webhook looks like
{
"event": "dispute.create",
"data": {
"id": 12345,
"refund_amount": 5000000,
"currency": "NGN",
"status": "awaiting-merchant-feedback",
"resolution": null,
"domain": "live",
"transaction": {
"id": 67890,
"reference": "txn_abc123",
"amount": 5000000,
"domain": "live",
"status": "success"
},
"message": "Customer claims they did not authorize this transaction",
"due_at": "2026-07-23T23:59:59.000Z",
"created_at": "2026-07-20T10:30:00.000Z"
}
}
The due_at field is your deadline. After that timestamp, the dispute is auto-resolved against you.
Chargebacks are governed by card network rules (Visa, Mastercard, Verve), not by Paystack. When you miss the response window:
Paystack cannot override the card network's decision. Even if you have clear proof that the transaction was legitimate, presenting evidence after the deadline is like submitting a court filing after the judgment. The process has concluded.
Some merchants ask about arbitration (a second level of dispute at the card network level). Arbitration is only available for disputes where you responded within the deadline and the resolution went against you despite your evidence. It is not available for missed deadlines.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
A missed dispute costs you:
For a business processing 1 million NGN per month, even a single missed dispute on a 50,000 NGN transaction is a 5% hit to monthly revenue. Two or three missed disputes and your monthly profit can evaporate.
The minimum viable dispute defense is a Slack alert that fires the moment a dispute is created. Here is the complete implementation:
// Dispute alert webhook handler
import crypto from 'crypto';
interface DisputeEvent {
event: 'dispute.create' | 'dispute.remind';
data: {
id: number;
refund_amount: number;
currency: string;
status: string;
due_at: string;
created_at: string;
message: string;
transaction: {
reference: string;
amount: number;
};
};
}
// Verify the webhook signature
function verifyPaystackSignature(body: string, signature: string): boolean {
const hash = crypto
.createHmac('sha512', process.env.PAYSTACK_SECRET_KEY!)
.update(body)
.digest('hex');
return hash === signature;
}
app.post('/api/webhooks/paystack', async (req, res) => {
const signature = req.headers['x-paystack-signature'] as string;
const rawBody = JSON.stringify(req.body);
if (!verifyPaystackSignature(rawBody, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Return 200 immediately
res.status(200).json({ received: true });
const event: DisputeEvent = req.body;
if (event.event === 'dispute.create') {
await handleNewDispute(event);
}
if (event.event === 'dispute.remind') {
await handleDisputeReminder(event);
}
});
async function handleNewDispute(event: DisputeEvent) {
const { data } = event;
const deadline = new Date(data.due_at);
const hoursRemaining = Math.round(
(deadline.getTime() - Date.now()) / (1000 * 60 * 60)
);
const amountFormatted = (data.refund_amount / 100).toLocaleString('en-NG', {
style: 'currency',
currency: data.currency,
});
// Send Slack alert
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: ':rotating_light: NEW PAYSTACK DISPUTE',
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: 'New Paystack Dispute - Action Required',
},
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Amount:*
${amountFormatted}` },
{ type: 'mrkdwn', text: `*Deadline:*
${hoursRemaining} hours` },
{ type: 'mrkdwn', text: `*Transaction:*
${data.transaction.reference}` },
{ type: 'mrkdwn', text: `*Dispute ID:*
${data.id}` },
],
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Customer message:*
${data.message || 'No message provided'}`,
},
},
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: 'View in Paystack' },
url: `https://dashboard.paystack.com/#/disputes/${data.id}`,
style: 'danger',
},
],
},
],
}),
});
// Also send email to the finance team
await sendEmail({
to: process.env.FINANCE_EMAIL!,
subject: `URGENT: Paystack Dispute - ${amountFormatted} - ${hoursRemaining}h deadline`,
body: `A new dispute has been filed for transaction ${data.transaction.reference}.
Amount: ${amountFormatted}
Deadline: ${deadline.toLocaleString()} (${hoursRemaining} hours from now)
Customer message: ${data.message || 'None'}
Respond in the Paystack dashboard:
https://dashboard.paystack.com/#/disputes/${data.id}
If you do not respond by the deadline, the dispute is automatically resolved
in the customer's favour and the amount is deducted from your balance.`,
});
// Record the dispute in your database
await db.query(
`INSERT INTO disputes (paystack_id, transaction_reference, amount, status, due_at, created_at)
VALUES ($1, $2, $3, 'awaiting_response', $4, NOW())`,
[data.id, data.transaction.reference, data.refund_amount, data.due_at]
);
}
async function handleDisputeReminder(event: DisputeEvent) {
const { data } = event;
const deadline = new Date(data.due_at);
const hoursRemaining = Math.round(
(deadline.getTime() - Date.now()) / (1000 * 60 * 60)
);
// Send escalation alert
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `:warning: DISPUTE REMINDER: ${hoursRemaining}h remaining for dispute ${data.id}. Respond NOW or lose ${(data.refund_amount / 100).toLocaleString()} ${data.currency}.`,
}),
});
}
A Slack alert is step one. You also need a process that ensures someone actually responds to the dispute. Here is a three-tier escalation:
Tier 1: Immediate (0 to 2 hours after dispute creation)
Tier 2: Warning (12 hours before deadline)
Tier 3: Critical (4 hours before deadline)
// Escalation checker: run every hour via cron
async function checkDisputeEscalation() {
const openDisputes = await db.query(
`SELECT * FROM disputes
WHERE status = 'awaiting_response'
AND due_at > NOW()`
);
for (const dispute of openDisputes.rows) {
const hoursRemaining = Math.round(
(new Date(dispute.due_at).getTime() - Date.now()) / (1000 * 60 * 60)
);
if (hoursRemaining <= 4 && !dispute.tier3_alerted) {
// Tier 3: Critical escalation
await sendSMS(process.env.CEO_PHONE!,
`CRITICAL: Paystack dispute for ${dispute.amount / 100} ${dispute.currency} expires in ${hoursRemaining}h. Respond NOW.`
);
await db.query(
'UPDATE disputes SET tier3_alerted = true WHERE id = $1',
[dispute.id]
);
} else if (hoursRemaining <= 12 && !dispute.tier2_alerted) {
// Tier 2: Warning escalation
await sendDirectSlackMessage(
process.env.FINANCE_HEAD_SLACK_ID!,
`Dispute ${dispute.paystack_id} for ${dispute.amount / 100} ${dispute.currency} has ${hoursRemaining}h remaining. No response yet.`
);
await db.query(
'UPDATE disputes SET tier2_alerted = true WHERE id = $1',
[dispute.id]
);
}
}
}
The best dispute is one that never happens. Most disputes are caused by:
Prevention checklist:
If your alert system works and you catch the dispute before the deadline, here is how to respond effectively:
Evidence that helps win disputes:
Test your dispute handling before a real dispute arrives:
dispute.create event to your webhook endpoint. Confirm that the Slack alert fires, the email is sent, and the dispute is recorded in your database.due_at set to 3 hours from now. Run the escalation checker. Confirm the Tier 3 critical alert fires.dispute.remind webhook. Confirm the reminder alert fires with the correct hours remaining.Do this test quarterly. People change roles, Slack channels get renamed, email addresses change. A dispute alert system that worked 6 months ago might not work today. For a complete reference on all Paystack errors, see the Paystack Errors and Troubleshooting: The Complete Reference.
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