/stripe-guides

How to manage disputes in Stripe?

Learn how to manage Stripe disputes step-by-step: set up notifications, gather evidence, respond via dashboard or API, track status, and prevent future chargebacks.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Book a free consultation

How to manage disputes in Stripe?

How to Manage Disputes in Stripe: A Comprehensive Guide

 

Disputes (often called chargebacks) occur when a customer questions a charge with their bank or credit card company. Managing disputes effectively is crucial for businesses using Stripe as their payment processor. This tutorial will walk you through handling disputes in Stripe from beginning to end.

 

Step 1: Understanding Stripe Disputes

 

Before diving into the management process, it's important to understand what disputes are in Stripe:

  • A dispute occurs when a customer contacts their card issuer to reverse a charge.
  • Disputes typically come with a fee (usually $15 USD in Stripe) regardless of the outcome.
  • Stripe automatically notifies you via email and dashboard notifications when a dispute occurs.
  • Disputes have deadlines - you must respond within the specified timeframe (usually 7-21 days).

 

Step 2: Setting Up Dispute Notifications

 

Ensure you're promptly notified about disputes:

  • Go to the Stripe Dashboard → Settings → Notifications.
  • Enable email notifications for disputes.
  • Consider setting up webhook endpoints to get programmatic notifications about disputes.

Here's how to set up a webhook for dispute events:


const stripe = require('stripe')('sk_test_YOUR_SECRET_KEY');
const express = require('express');
const app = express();

app.post('/webhook', express.raw({type: 'application/json'}), (request, response) => {
  const sig = request.headers['stripe-signature'];
  
  let event;
  
  try {
    event = stripe.webhooks.constructEvent(
      request.body,
      sig,
      'whsec_YOUR_WEBHOOK\_SECRET'
    );
  } catch (err) {
    return response.status(400).send(`Webhook Error: ${err.message}`);
  }
  
  // Handle dispute events
  if (event.type === 'charge.dispute.created') {
    const dispute = event.data.object;
    console.log(`New dispute received with ID: ${dispute.id}`);
    // Add your dispute handling logic here
  }
  
  if (event.type === 'charge.dispute.updated') {
    const dispute = event.data.object;
    console.log(`Dispute updated: ${dispute.id}, Status: ${dispute.status}`);
    // Add your update handling logic here
  }
  
  response.status(200).send({received: true});
});

app.listen(3000, () => console.log('Running on port 3000'));

 

Step 3: Accessing Dispute Information

 

When a dispute occurs, you need to access its details:

  • Log in to your Stripe Dashboard.
  • Navigate to Payments → Disputes in the sidebar.
  • Click on the specific dispute to view its details.

Alternatively, use the API to retrieve dispute information:


const stripe = require('stripe')('sk_test_YOUR_SECRET_KEY');

// 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);
    throw error;
  }
}

// List all disputes
async function listDisputes() {
  try {
    const disputes = await stripe.disputes.list({
      limit: 10,
      // Optional filters
      // created: { gte: Math.floor(Date.now() / 1000) - 86400 }, // Last 24 hours
      // status: 'needs\_response'
    });
    console.log('Disputes:', disputes.data);
    return disputes;
  } catch (error) {
    console.error('Error listing disputes:', error);
    throw error;
  }
}

// Execute functions
getDisputeDetails('dp\_123456789');
listDisputes();

 

Step 4: Understanding Dispute Types and Evidence Requirements

 

Different dispute types require different evidence. Here are the common categories:

  • Fraudulent: Customer claims they didn't authorize the purchase
  • Product not received: Customer claims they didn't receive products/services
  • Product unacceptable: Customer claims product was defective or not as described
  • Duplicate: Customer claims they were charged multiple times
  • Subscription canceled: Customer claims they canceled a subscription
  • Credit not processed: Customer claims a refund wasn't processed
  • General: Other dispute reasons

 

Step 5: Preparing Your Evidence

 

Gather evidence based on the dispute type. Common evidence includes:

  • Order details and transaction records
  • Customer communications
  • Shipping/tracking information
  • Proof of delivery
  • Service usage logs
  • Subscription agreements
  • Screenshots of your refund policy
  • Product descriptions and images

 

Step 6: Submitting Your Dispute Response

 

Submit your evidence through the Stripe Dashboard:

  • Go to the specific dispute in your dashboard
  • Click on "Submit Evidence"
  • Fill out the relevant fields based on the dispute type
  • Upload supporting documents (PDFs, images)
  • Submit your response before the deadline

Alternatively, use the API to submit evidence:


const stripe = require('stripe')('sk_test_YOUR_SECRET_KEY');

async function submitDisputeEvidence(disputeId) {
  try {
    const dispute = await stripe.disputes.update(
      disputeId,
      {
        evidence: {
          customer\_name: 'John Smith',
          customer\_email: '[email protected]',
          customer_purchase_ip: '192.168.0.1',
          customer\_signature: 'https://example.com/signature.png',
          billing\_address: '123 Main St, Anytown, AT 12345',
          receipt: 'https://example.com/receipt.pdf',
          service\_date: '2023-01-15',
          shipping\_address: '123 Main St, Anytown, AT 12345',
          shipping\_date: '2023-01-16',
          shipping\_carrier: 'USPS',
          shipping_tracking_number: '1Z999AA10123456784',
          product\_description: 'High-quality merchandise as described on website',
          uncategorized\_text: 'Additional evidence information that doesn't fit into other categories'
        },
        submit: true // Set to true to submit evidence immediately
      }
    );
    
    console.log('Evidence submitted successfully:', dispute.status);
    return dispute;
  } catch (error) {
    console.error('Error submitting evidence:', error);
    throw error;
  }
}

// Execute the function
submitDisputeEvidence('dp\_123456789');

 

Step 7: Tracking Dispute Status

 

After submitting evidence, monitor the dispute status:

  • Warning needs response: You need to submit evidence
  • Response submitted: You've responded, awaiting decision
  • Won: The dispute was resolved in your favor
  • Lost: The dispute was resolved in the customer's favor
  • Under review: The card issuer is reviewing the case

You can check the status via the dashboard or API:


const stripe = require('stripe')('sk_test_YOUR_SECRET_KEY');

async function checkDisputeStatus(disputeId) {
  try {
    const dispute = await stripe.disputes.retrieve(disputeId);
    console.log(`Dispute status: ${dispute.status}`);
    console.log(`Dispute amount: ${dispute.amount / 100} ${dispute.currency}`);
    console.log(`Evidence due by: ${new Date(dispute.evidence_details.due_by * 1000).toLocaleDateString()}`);
    return dispute;
  } catch (error) {
    console.error('Error checking dispute status:', error);
    throw error;
  }
}

// Execute the function
checkDisputeStatus('dp\_123456789');

 

Step 8: Implementing Preventative Measures

 

To reduce future disputes, implement these preventative measures:

  • Use Stripe Radar for fraud prevention
  • Implement 3D Secure authentication for high-risk transactions
  • Ensure clear product descriptions and transparent policies
  • Keep detailed records of all transactions and customer interactions
  • Use recognizable billing descriptors so customers recognize charges
  • Provide excellent customer service and respond quickly to complaints
  • Use shipping with tracking and delivery confirmation

Here's how to enable Radar rules and 3D Secure:


const stripe = require('stripe')('sk_test_YOUR_SECRET_KEY');

// Create a PaymentIntent with 3D Secure authentication
async function createSecurePayment(amount, currency, customerId) {
  try {
    const paymentIntent = await stripe.paymentIntents.create({
      amount: amount,
      currency: currency,
      customer: customerId,
      payment_method_types: ['card'],
      setup_future_usage: 'off\_session',
      confirmation\_method: 'automatic',
      confirm: true,
      return\_url: 'https://example.com/return',
      payment_method_options: {
        card: {
          request_three_d\_secure: 'any' // Options: 'automatic', 'any', 'required'
        }
      },
      metadata: {
        order_id: 'ORDER_12345',
        customer\_name: 'John Smith'
      }
    });
    
    console.log('Payment intent created:', paymentIntent.id);
    return paymentIntent;
  } catch (error) {
    console.error('Error creating secure payment:', error);
    throw error;
  }
}

// Execute the function
createSecurePayment(2000, 'usd', 'cus\_123456789');

 

Step 9: Analyzing Dispute Patterns

 

Regularly analyze your disputes to identify patterns:

  • Track dispute rates by product, customer segment, or geography
  • Look for common dispute reasons
  • Identify high-risk transaction types
  • Review your win/loss ratio for different dispute categories

Use the Stripe API to gather dispute analytics:


const stripe = require('stripe')('sk_test_YOUR_SECRET_KEY');

async function analyzeDisputePatterns() {
  try {
    // Get all 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({
      limit: 100,
      created: {
        gte: startDate,
        lte: endDate
      },
      expand: ['data.charge']
    });
    
    // Basic analysis
    const totalDisputes = disputes.data.length;
    const disputesByReason = {};
    const disputesByStatus = {};
    const disputesByProduct = {};
    
    disputes.data.forEach(dispute => {
      // Count by reason
      const reason = dispute.reason || 'unknown';
      disputesByReason[reason] = (disputesByReason[reason] || 0) + 1;
      
      // Count by status
      const status = dispute.status || 'unknown';
      disputesByStatus[status] = (disputesByStatus[status] || 0) + 1;
      
      // Count by product (if you store product info in metadata)
      if (dispute.charge && dispute.charge.metadata && dispute.charge.metadata.product\_id) {
        const productId = dispute.charge.metadata.product\_id;
        disputesByProduct[productId] = (disputesByProduct[productId] || 0) + 1;
      }
    });
    
    console.log('Dispute Analysis:');
    console.log('Total Disputes:', totalDisputes);
    console.log('By Reason:', disputesByReason);
    console.log('By Status:', disputesByStatus);
    console.log('By Product:', disputesByProduct);
    
    return {
      totalDisputes,
      disputesByReason,
      disputesByStatus,
      disputesByProduct
    };
  } catch (error) {
    console.error('Error analyzing disputes:', error);
    throw error;
  }
}

// Execute the function
analyzeDisputePatterns();

 

Step 10: Automating Dispute Management

 

For businesses with high transaction volume, automate parts of the dispute management process:

  • Set up automated evidence submission for common dispute types
  • Create templates for different dispute categories
  • Implement real-time alerts and response systems
  • Consider using Stripe's machine learning tools to predict and prevent disputes

Here's an example of automated dispute handling with Node.js:


const stripe = require('stripe')('sk_test_YOUR_SECRET_KEY');
const express = require('express');
const app = express();

// Your database or order system integration
const orderSystem = {
  getOrderDetails: async (orderId) => {
    // Mock implementation - replace with your actual order system
    return {
      id: orderId,
      customer: {
        name: 'John Smith',
        email: '[email protected]',
        ip: '192.168.1.1'
      },
      shipping: {
        carrier: 'FedEx',
        trackingNumber: '123456789012',
        address: '123 Main St, Anytown, AT 12345',
        deliveryDate: '2023-01-20'
      },
      product: {
        description: 'Premium Widget X1000',
        details: 'High-quality widget with 1-year warranty'
      },
      receipt\_url: 'https://example.com/receipts/order-123.pdf',
      date: '2023-01-15'
    };
  }
};

app.post('/webhook', express.raw({type: 'application/json'}), async (request, response) => {
  const sig = request.headers['stripe-signature'];
  
  let event;
  
  try {
    event = stripe.webhooks.constructEvent(
      request.body,
      sig,
      'whsec_YOUR_WEBHOOK\_SECRET'
    );
  } catch (err) {
    return response.status(400).send(`Webhook Error: ${err.message}`);
  }
  
  // Handle dispute creation
  if (event.type === 'charge.dispute.created') {
    const dispute = event.data.object;
    console.log(`New dispute received with ID: ${dispute.id}`);
    
    try {
      // Get charge information to find the associated order
      const charge = await stripe.charges.retrieve(dispute.charge);
      
      // Assuming you store the order ID in charge metadata
      if (charge.metadata && charge.metadata.order\_id) {
        const orderId = charge.metadata.order\_id;
        
        // Get order details from your system
        const orderDetails = await orderSystem.getOrderDetails(orderId);
        
        // Prepare evidence based on dispute reason
        let evidenceParams = {
          customer\_name: orderDetails.customer.name,
          customer\_email: orderDetails.customer.email,
          customer_purchase_ip: orderDetails.customer.ip,
          receipt: orderDetails.receipt\_url,
          service\_date: orderDetails.date,
          product\_description: orderDetails.product.description
        };
        
        // Add specific evidence based on dispute reason
        if (dispute.reason === 'product_not_received') {
          evidenceParams = {
            ...evidenceParams,
            shipping\_carrier: orderDetails.shipping.carrier,
            shipping_tracking_number: orderDetails.shipping.trackingNumber,
            shipping\_address: orderDetails.shipping.address,
            shipping\_date: orderDetails.date
          };
        } else if (dispute.reason === 'product\_unacceptable') {
          evidenceParams = {
            ...evidenceParams,
            product\_description: `${orderDetails.product.description}: ${orderDetails.product.details}`
          };
        }
        
        // Submit the evidence
        const updatedDispute = await stripe.disputes.update(
          dispute.id,
          {
            evidence: evidenceParams,
            submit: true // Automatically submit the evidence
          }
        );
        
        console.log(`Automated response submitted for dispute ${dispute.id}`);
      } else {
        console.log(`Order ID not found for dispute ${dispute.id}, manual handling required`);
        // Implement notification for manual handling
      }
    } catch (error) {
      console.error('Error handling dispute:', error);
    }
  }
  
  response.status(200).send({received: true});
});

app.listen(3000, () => console.log('Running on port 3000'));

 

Conclusion

 

Managing disputes in Stripe requires a systematic approach from prevention to resolution. By following these steps and implementing best practices, you can reduce the frequency of disputes, improve your chances of winning legitimate challenges, and minimize the impact on your business. Remember to keep detailed records, respond promptly, and continuously analyze patterns to refine your dispute management strategy.

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Book a Free Consultation

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022