Learn how to handle Stripe chargebacks step-by-step: set up notifications, prevent disputes, gather evidence, submit responses, and automate your dispute process.
Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
How to Handle Chargebacks in Stripe: A Comprehensive Tutorial
Introduction
Chargebacks occur when customers dispute transactions with their card issuer rather than requesting a refund directly from you. In Stripe, these appear as "disputes" and require careful handling to maximize your chances of winning. This tutorial will guide you through the complete process of managing chargebacks in Stripe, from prevention to resolution.
Step 1: Understanding Stripe's Dispute Process
Before diving into handling chargebacks, it's important to understand how Stripe processes disputes:
Step 2: Setting Up Dispute Notifications
First, ensure you're promptly notified when disputes occur:
Here's how to set up a webhook endpoint to listen for dispute events:
const express = require('express');
const app = express();
const stripe = require('stripe')('sk_test_yourSecretKey');
app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
'whsec\_yourWebhookSecret'
);
} catch (err) {
console.error(`Webhook Error: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the dispute.created event
if (event.type === 'dispute.created') {
const dispute = event.data.object;
console.log(`New dispute received: ${dispute.id} for charge ${dispute.charge}`);
// Start your dispute handling process
handleNewDispute(dispute);
}
res.status(200).send({received: true});
});
function handleNewDispute(dispute) {
// Logic to gather evidence, notify team members, etc.
console.log(`Dispute amount: ${dispute.amount}, reason: ${dispute.reason}`);
// Further processing code...
}
app.listen(3000, () => console.log('Running on port 3000'));
Step 3: Implementing Chargeback Prevention Measures
Prevention is better than cure. Implement these preventive measures:
To enable 3D Secure for charges:
// On the server
const paymentIntent = await stripe.paymentIntents.create({
amount: 1000,
currency: 'usd',
payment_method_types: ['card'],
payment_method: 'pm_card\_threeDSecure2Required',
confirm: true,
return\_url: 'https://yourwebsite.com/return-url',
});
// On the client-side (using Stripe.js)
const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
payment\_method: {
card: elements.getElement('card'),
billing\_details: {
name: 'Jenny Rosen',
email: '[email protected]'
},
},
});
Step 4: Accessing and Reviewing Disputes
When a dispute occurs:
const stripe = require('stripe')('sk_test_yourSecretKey');
// Retrieve a specific dispute
async function getDisputeDetails(disputeId) {
try {
const dispute = await stripe.disputes.retrieve(disputeId);
console.log(`Dispute details:`, dispute);
return dispute;
} catch (error) {
console.error(`Error retrieving dispute: ${error.message}`);
}
}
// List all disputes
async function listAllDisputes() {
try {
const disputes = await stripe.disputes.list({
limit: 100,
status: 'needs\_response' // Filter by status
});
console.log(`Found ${disputes.data.length} disputes that need response`);
return disputes;
} catch (error) {
console.error(`Error listing disputes: ${error.message}`);
}
}
// Example usage
getDisputeDetails('dp\_1ABCxyz123456');
listAllDisputes();
Step 5: Gathering Evidence for Dispute Response
Collect appropriate evidence based on the dispute reason:
Create a centralized evidence repository function:
async function collectDisputeEvidence(charge_id, dispute_id) {
// Retrieve related charge data
const charge = await stripe.charges.retrieve(charge\_id);
// Retrieve order details from your database
const order = await database.getOrderByPaymentId(charge\_id);
// Retrieve customer data
const customer = await database.getCustomer(order.customer\_id);
// Gather shipping information if applicable
const shipping = await database.getShippingDetails(order.id);
// Collect communication history
const communications = await database.getCustomerCommunications(order.customer\_id);
// Format evidence based on dispute reason
return {
customer\_name: customer.name,
customer\_email: customer.email,
customer_purchase_ip: order.ip\_address,
shipping_address: shipping.formatted_address,
shipping_date: shipping.ship_date,
shipping_tracking_number: shipping.tracking\_number,
shipping\_carrier: shipping.carrier,
product\_description: order.items.map(item => item.description).join(', '),
receipt_or_invoice_id: order.invoice_number,
customer_signature: shipping.delivery_signature,
service_date: order.service_date || order.created\_at,
// Additional evidence fields based on dispute reason...
};
}
Step 6: Submitting Evidence to Stripe
Submit your collected evidence through Stripe's Dashboard or API:
Here's how to submit evidence via API:
const stripe = require('stripe')('sk_test_yourSecretKey');
async function submitDisputeEvidence(disputeId, evidence) {
try {
const dispute = await stripe.disputes.update(
disputeId,
{
evidence: {
customer_name: evidence.customer_name,
customer_email_address: evidence.customer\_email,
customer_purchase_ip: evidence.customer_purchase_ip,
customer_signature: evidence.customer_signature,
billing_address: evidence.billing_address,
receipt: evidence.receipt_or_invoice\_id,
product_description: evidence.product_description,
shipping_address: evidence.shipping_address,
shipping_date: evidence.shipping_date,
shipping_tracking_number: evidence.shipping_tracking_number,
shipping_carrier: evidence.shipping_carrier,
service_date: evidence.service_date,
duplicate_charge_id: evidence.duplicate_charge_id,
duplicate_charge_documentation: evidence.duplicate_charge_documentation,
cancellation_policy: evidence.cancellation_policy,
cancellation_policy_disclosure: evidence.cancellation_policy_disclosure,
cancellation_rebuttal: evidence.cancellation_rebuttal,
refund_policy: evidence.refund_policy,
refund_policy_disclosure: evidence.refund_policy_disclosure,
refund_refusal_explanation: evidence.refund_refusal_explanation,
access_activity_log: evidence.access_activity_log,
// Additional evidence fields...
},
submit: true // Set to false if you want to save without submitting
}
);
console.log(`Evidence submitted for dispute ${disputeId}`);
return dispute;
} catch (error) {
console.error(`Error submitting dispute evidence: ${error.message}`);
throw error;
}
}
// Example usage
const evidence = await collectDisputeEvidence('ch_1234567890', 'dp_1ABCxyz123456');
submitDisputeEvidence('dp\_1ABCxyz123456', evidence);
Step 7: Tracking Dispute Status
After submitting evidence, track the dispute status:
Set up a status monitoring system:
const stripe = require('stripe')('sk_test_yourSecretKey');
// Set up a webhook handler for dispute status updates
app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
'whsec\_yourWebhookSecret'
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle dispute status updates
if (event.type === 'dispute.updated') {
const dispute = event.data.object;
await updateDisputeStatus(dispute);
} else if (event.type === 'dispute.closed') {
const dispute = event.data.object;
await finalizeDisputeOutcome(dispute);
}
res.status(200).send({received: true});
});
async function updateDisputeStatus(dispute) {
// Update dispute status in your database
await database.updateDisputeStatus(dispute.id, dispute.status);
// Notify relevant team members
if (dispute.status === 'warning_under_review') {
await notifyTeam(`Dispute ${dispute.id} is now under review by the card network.`);
}
}
async function finalizeDisputeOutcome(dispute) {
// Record the final outcome
await database.updateDisputeOutcome(dispute.id, dispute.status);
// Notify team and take appropriate actions
if (dispute.status === 'lost') {
await notifyTeam(`Dispute ${dispute.id} was lost. Amount: ${dispute.amount/100} ${dispute.currency}`);
// Update financials, analyze why the dispute was lost
await updateFinancials(dispute.id, dispute.amount, 'lost');
} else if (dispute.status === 'won') {
await notifyTeam(`Dispute ${dispute.id} was won! Amount recovered: ${dispute.amount/100} ${dispute.currency}`);
await updateFinancials(dispute.id, dispute.amount, 'won');
}
}
Step 8: Implementing a Dispute Analytics System
Analyze dispute patterns to improve prevention:
async function analyzeDisputes() {
try {
// Get disputes from the last 90 days
const endDate = Math.floor(Date.now() / 1000);
const startDate = endDate - (90 _ 24 _ 60 \* 60);
const disputes = await stripe.disputes.list({
created: {
gte: startDate,
lte: endDate
},
limit: 100,
expand: ['data.charge']
});
// Categorize disputes by reason
const reasonCounts = {};
const outcomeByReason = {};
let totalAmount = 0;
let lostAmount = 0;
disputes.data.forEach(dispute => {
// Count by reason
reasonCounts[dispute.reason] = (reasonCounts[dispute.reason] || 0) + 1;
// Track outcomes by reason
if (!outcomeByReason[dispute.reason]) {
outcomeByReason[dispute.reason] = { won: 0, lost: 0, pending: 0 };
}
if (dispute.status === 'won') {
outcomeByReason[dispute.reason].won += 1;
} else if (dispute.status === 'lost') {
outcomeByReason[dispute.reason].lost += 1;
lostAmount += dispute.amount;
} else {
outcomeByReason[dispute.reason].pending += 1;
}
totalAmount += dispute.amount;
});
// Calculate dispute rate
const charges = await stripe.charges.list({
created: {
gte: startDate,
lte: endDate
},
limit: 1
});
const totalChargeCount = charges.total\_count;
const disputeRate = (disputes.data.length / totalChargeCount) \* 100;
return {
total\_disputes: disputes.data.length,
dispute\_rate: disputeRate.toFixed(2) + '%',
total_disputed_amount: totalAmount / 100,
total_lost_amount: lostAmount / 100,
reasons: reasonCounts,
outcomes_by_reason: outcomeByReason
};
} catch (error) {
console.error(`Error analyzing disputes: ${error.message}`);
}
}
Step 9: Automating the Dispute Response Process
For scale, automate your dispute handling:
const stripe = require('stripe')('sk_test_yourSecretKey');
const express = require('express');
const app = express();
// Automated dispute handling system
class DisputeHandler {
constructor(database, notificationService) {
this.database = database;
this.notifications = notificationService;
}
async processNewDispute(dispute) {
try {
// Log the dispute
await this.database.logDispute(dispute);
// Notify team
await this.notifications.alertTeam(dispute);
// Determine dispute category and required evidence
const evidenceRequirements = this.determineEvidenceRequirements(dispute.reason);
// Gather available evidence automatically
const evidence = await this.gatherEvidence(dispute.charge, evidenceRequirements);
// Determine if we can automatically respond or need human review
if (this.canAutoRespond(evidence, evidenceRequirements)) {
// Submit evidence automatically
await this.submitEvidence(dispute.id, evidence);
await this.database.updateDisputeStatus(dispute.id, 'auto\_responded');
await this.notifications.notifyAutoResponse(dispute.id);
} else {
// Flag for human review
await this.database.updateDisputeStatus(dispute.id, 'needs\_review');
await this.notifications.requestHumanReview(dispute.id, evidenceRequirements);
}
return { success: true, status: 'processed' };
} catch (error) {
console.error(`Error processing dispute ${dispute.id}: ${error.message}`);
await this.notifications.reportError(dispute.id, error);
return { success: false, error: error.message };
}
}
determineEvidenceRequirements(reason) {
const evidenceMap = {
'fraudulent': ['customer_details', 'ip_logs', 'device_info', 'prior_transactions'],
'product_not_received': ['shipping_info', 'tracking_number', 'delivery\_confirmation'],
'product_unacceptable': ['product_description', 'photos', 'communications'],
'duplicate': ['transaction_comparison', 'order_details'],
'subscription_canceled': ['terms_acceptance', 'cancellation_policy', 'usage_logs'],
'unrecognized': ['transaction_receipt', 'business_details'],
'credit_not_processed': ['refund_policy', 'communications', 'cancellation_records'],
'general': ['receipt', 'customer\_info', 'communications']
};
return evidenceMap[reason] || evidenceMap['general'];
}
async gatherEvidence(chargeId, requiredEvidence) {
// Get charge details
const charge = await stripe.charges.retrieve(chargeId, {
expand: ['customer', 'payment\_method']
});
// Get order from your database
const order = await this.database.getOrderByChargeId(chargeId);
// Build evidence object based on requirements
const evidence = {};
for (const item of requiredEvidence) {
switch(item) {
case 'customer\_details':
evidence.customer_name = order?.customer?.name || charge.billing_details?.name;
evidence.customer_email_address = order?.customer?.email || charge.billing\_details?.email;
evidence.billing_address = JSON.stringify(charge.billing_details?.address);
break;
case 'ip\_logs':
evidence.customer_purchase_ip = order?.ip\_address;
break;
case 'shipping\_info':
if (order?.shipping) {
evidence.shipping\_address = JSON.stringify(order.shipping.address);
evidence.shipping_date = order.shipping.ship_date;
evidence.shipping\_carrier = order.shipping.carrier;
evidence.shipping_tracking_number = order.shipping.tracking\_number;
}
break;
// Add more evidence gathering logic for each category
}
}
return evidence;
}
canAutoRespond(evidence, requirements) {
// Check if we have all critical evidence pieces
const criticalEvidence = requirements.filter(item =>
['shipping_info', 'tracking_number', 'customer\_details'].includes(item));
// Check if all critical evidence is available
return criticalEvidence.every(item => {
switch(item) {
case 'shipping\_info':
return evidence.shipping_address && evidence.shipping_date;
case 'tracking\_number':
return evidence.shipping_tracking_number;
case 'customer\_details':
return evidence.customer_name && evidence.customer_email\_address;
default:
return false;
}
});
}
async submitEvidence(disputeId, evidence) {
return await stripe.disputes.update(
disputeId,
{
evidence: evidence,
submit: true
}
);
}
}
// Set up webhook for automatic processing
app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, 'whsec\_yourWebhookSecret');
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
if (event.type === 'dispute.created') {
const disputeHandler = new DisputeHandler(database, notificationService);
await disputeHandler.processNewDispute(event.data.object);
}
res.status(200).send({received: true});
});
Step 10: Learning from Dispute Outcomes
Continuously improve your dispute handling:
Implement a feedback loop:
async function disputeLearningSystem() {
// Get recent dispute outcomes
const recentDisputes = await stripe.disputes.list({
created: {
gte: Math.floor(Date.now() / 1000) - (30 _ 24 _ 60 \* 60) // Last 30 days
},
limit: 100,
status: 'won'
});
// Analyze successful evidence submissions
const successfulEvidenceTypes = {};
const successfulEvidenceByReason = {};
for (const dispute of recentDisputes.data) {
// Get the full dispute with evidence
const fullDispute = await stripe.disputes.retrieve(dispute.id);
// Record which evidence fields were submitted
const submittedEvidence = Object.keys(fullDispute.evidence).filter(
key => fullDispute.evidence[key] !== null
);
// Count frequency of each evidence type
submittedEvidence.forEach(evidenceType => {
successfulEvidenceTypes[evidenceType] = (successfulEvidenceTypes[evidenceType] || 0) + 1;
// Organize by dispute reason
if (!successfulEvidenceByReason[dispute.reason]) {
successfulEvidenceByReason[dispute.reason] = {};
}
successfulEvidenceByReason\[dispute.reason]\[evidenceType] =
(successfulEvidenceByReason\[dispute.reason]\[evidenceType] || 0) + 1;
});
}
// Update evidence templates based on findings
for (const reason in successfulEvidenceByReason) {
const mostEffectiveEvidence = Object.entries(successfulEvidenceByReason[reason])
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(entry => entry[0]);
await database.updateEvidenceTemplate(reason, mostEffectiveEvidence);
}
// Report on findings
return {
successful_evidence_types: successfulEvidenceTypes,
evidence_by_reason: successfulEvidenceByReason
};
}
Conclusion
Handling chargebacks effectively is crucial for maintaining your business's financial health. By implementing a systematic approach to chargeback prevention, evidence collection, and dispute management, you can significantly improve your chances of winning disputes and reduce their frequency over time.
Remember that each dispute is an opportunity to learn about potential issues in your business processes. Regular analysis and continuous improvement of your chargeback handling system will yield long-term benefits for your business.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.